Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions .github/workflows/sandbox-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: Sandbox Image

# Build the official Kit sandbox image and publish it to GHCR. The image is a
# ready-to-run Linux userland (Go + kit + gh/glab/tea + git/ssh) intended to be
# registered as a workdir.dev custom image:
#
# POST /v1/images
# { "source": { "type": "oci",
# "image_ref": "ghcr.io/mark3labs/kit-sandbox:latest" },
# "name": "custom/mark3labs/kit-sandbox" }
#
# Then create sandboxes with { "image": "custom/mark3labs/kit-sandbox", ... }.

on:
push:
branches: [master]
paths:
- "deploy/sandbox/**"
- ".github/workflows/sandbox-image.yml"
# Rebuild on every release so the image tracks the latest kit binary.
release:
types: [published]
workflow_dispatch:
inputs:
kit_version:
description: "kit version to install (go install ...@VERSION)"
required: false
default: "latest"

concurrency:
group: sandbox-image-${{ github.ref }}
cancel-in-progress: true

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}-sandbox # ghcr.io/mark3labs/kit-sandbox

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
# Actions are pinned to immutable commit SHAs (comments preserve the
# human-readable version) so an upstream retag can't alter this
# package-publishing pipeline.
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: Resolve kit version
id: kitver
env:
EVENT_NAME: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name || '' }}
INPUT_KIT_VERSION: ${{ inputs.kit_version || '' }}
run: |
if [ "$EVENT_NAME" = "release" ]; then
version="$RELEASE_TAG"
elif [ -n "$INPUT_KIT_VERSION" ]; then
version="$INPUT_KIT_VERSION"
else
version="latest"
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Reject anything that could smuggle shell/newlines into later steps.
case "$version" in
*$'\n'*|*$'\r'*|*' '*|*';'*|*'&'*|*'|'*|*'$'*|*'`'*)
echo "invalid kit version: $version" >&2; exit 1 ;;
esac
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"

- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3

- name: Log in to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata (tags + labels)
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# prefix=v keeps release image tags as vX.Y.Z / vX.Y; without it
# docker/metadata-action strips the leading v (v1.2.3 -> 1.2.3).
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=sha,format=short
type=semver,pattern={{version}},prefix=v,event=release
type=semver,pattern={{major}}.{{minor}},prefix=v,event=release

- name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
file: deploy/sandbox/Dockerfile
# workdir runs x86_64 Firecracker microVMs.
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
KIT_VERSION=${{ steps.kitver.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
115 changes: 115 additions & 0 deletions deploy/sandbox/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# syntax=docker/dockerfile:1
#
# Official Kit sandbox image.
#
# A ready-to-run Linux userland for the Kit coding agent, designed to be
# registered as a workdir.dev custom image (`POST /v1/images` with
# `source.type = "oci"`). It pre-bakes the toolchain that the dev-sandbox
# integration otherwise installs at runtime on every boot:
#
# * Go (compiler + toolchain) and the Kit CLI (`kit`)
# * The three git-forge CLIs: gh (GitHub), glab (GitLab), tea (Gitea)
# * git + openssh-client for SSH-based clones
# * the `127.0.0.1 localhost` /etc/hosts entry Kit's OAuth listener needs
#
# The base apt layer mirrors workdir's curated base image
# (deploy/images/base/Dockerfile in mv37-org/workdir). We deliberately do NOT
# COPY in workdir's `sandbox-guest-agent` / `sandbox-init`: workdir's custom
# image builder injects the (static musl) guest agent + init itself when it
# converts this OCI image to an ext4 rootfs.
#
# Build (linux/amd64 only — workdir runs x86_64 Firecracker, and the pinned
# SHA256 checksums below are amd64-specific):
# docker buildx build --platform linux/amd64 -f deploy/sandbox/Dockerfile -t kit-sandbox .
#
FROM --platform=linux/amd64 ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive

# Pinned tool versions — override at build time with --build-arg. Each download
# is verified against the matching SHA256 so a tampered mirror/asset fails the
# build instead of shipping. When bumping a version, update its *_SHA256 too
# (the upstream checksums: go.dev/dl JSON, gh_<v>_checksums.txt,
# glab checksums.txt, tea-<v>-linux-amd64.sha256).
ARG GO_VERSION=1.26.4
ARG GO_SHA256=1153d3d50e0ac764b447adfe05c2bcf08e889d42a02e0fe0259bd47f6733ad7f
ARG KIT_VERSION=latest
ARG GH_VERSION=2.95.0
ARG GH_SHA256=25d1e4729e8808c9ed3d613e96ebd3f3e44446f2d368c89d878a71a36ddb3d8c
ARG GLAB_VERSION=1.105.0
ARG GLAB_SHA256=d8c92c640d7adf84c9dd01d1621fca45471fb61454f025c2fa3046dfc52d0d2f
ARG TEA_VERSION=0.14.1
ARG TEA_SHA256=3cf7c5d1c20808c9ba2efb9ac125cee10d969daf398e653ea2b33cde201ea317

# amd64 only (checksums above are amd64; FROM pins the platform).
ARG TARGETARCH=amd64

# Base userland — mirrors workdir's curated base image apt layer, plus
# openssh-client (SSH clones) and unzip/jq/sudo conveniences.
RUN apt-get update && apt-get install -y --no-install-recommends \
bash coreutils curl wget ca-certificates git openssh-client \
socat iproute2 iputils-ping unzip jq sudo \
python3 python3-pip python3-venv nodejs npm build-essential \
&& rm -rf /var/lib/apt/lists/*

# Go toolchain -> /usr/local/go, on PATH for all later layers and at runtime.
RUN curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${TARGETARCH}.tar.gz" \
-o /tmp/go.tgz \
&& echo "${GO_SHA256} /tmp/go.tgz" | sha256sum -c - \
&& tar -C /usr/local -xzf /tmp/go.tgz \
&& rm /tmp/go.tgz
ENV PATH=/usr/local/go/bin:/usr/local/bin:/root/go/bin:$PATH
ENV GOPATH=/root/go
ENV GOTOOLCHAIN=auto

# Kit CLI — compiled from source and installed onto PATH as `kit`. Inject the
# version ldflag (matching .goreleaser.yaml) so `kit --version` reports the
# build instead of "dev"; on release builds KIT_VERSION is the real tag.
RUN GOBIN=/usr/local/bin go install \
-ldflags "-s -w -X main.version=${KIT_VERSION}" \
"github.com/mark3labs/kit/cmd/kit@${KIT_VERSION}" \
&& kit --version

# GitHub CLI (gh).
RUN curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${TARGETARCH}.tar.gz" \
-o /tmp/gh.tgz \
&& echo "${GH_SHA256} /tmp/gh.tgz" | sha256sum -c - \
&& tar -C /tmp -xzf /tmp/gh.tgz \
&& install -m755 "/tmp/gh_${GH_VERSION}_linux_${TARGETARCH}/bin/gh" /usr/local/bin/gh \
&& rm -rf /tmp/gh.tgz "/tmp/gh_${GH_VERSION}_linux_${TARGETARCH}"

# GitLab CLI (glab).
RUN curl -fsSL "https://gitlab.com/gitlab-org/cli/-/releases/v${GLAB_VERSION}/downloads/glab_${GLAB_VERSION}_linux_${TARGETARCH}.tar.gz" \
-o /tmp/glab.tgz \
&& echo "${GLAB_SHA256} /tmp/glab.tgz" | sha256sum -c - \
&& mkdir -p /tmp/glab \
&& tar -C /tmp/glab -xzf /tmp/glab.tgz \
&& install -m755 /tmp/glab/bin/glab /usr/local/bin/glab \
&& rm -rf /tmp/glab.tgz /tmp/glab

# Gitea CLI (tea) — single static binary.
RUN curl -fsSL "https://dl.gitea.com/tea/${TEA_VERSION}/tea-${TEA_VERSION}-linux-${TARGETARCH}" \
-o /tmp/tea \
&& echo "${TEA_SHA256} /tmp/tea" | sha256sum -c - \
&& install -m755 /tmp/tea /usr/local/bin/tea \
&& rm -f /tmp/tea

# Kit's OAuth callback listener binds localhost; without this entry it fails to
# create the listener and nil-panics on cleanup. Bake it in so the runtime
# doesn't have to patch /etc/hosts on every boot.
RUN grep -q localhost /etc/hosts || echo '127.0.0.1 localhost' >> /etc/hosts

# Default workspace the agent clones into.
RUN mkdir -p /workspace
WORKDIR /workspace

# Smoke-check that every CLI resolves on PATH at build time. No `|| true`:
# `kit` is the main payload, so a broken binary must fail the build.
RUN set -e; \
go version; \
kit --version; \
gh --version; \
glab --version; \
tea --version

CMD ["/bin/bash"]
84 changes: 84 additions & 0 deletions deploy/sandbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Kit sandbox image

An official, pre-baked Linux image for running the [Kit](https://github.com/mark3labs/kit)
coding agent inside a [workdir.dev](https://workdir.dev) sandbox.

It exists so the dev-sandbox integration no longer has to `apt-get install` and
`go install` the toolchain on **every** sandbox boot — everything is baked in.

## What's inside

Based on `ubuntu:24.04`, mirroring workdir's curated base apt layer, plus:

| Tool | Purpose |
| --- | --- |
| **Go** (`/usr/local/go`) | toolchain for building/running Go code |
| **kit** | the Kit coding agent CLI (`go install github.com/mark3labs/kit/cmd/kit`) |
| **gh** | GitHub CLI — open PRs, manage repos |
| **glab** | GitLab CLI — open MRs |
| **tea** | Gitea CLI |
| **git**, **openssh-client** | SSH-based clones |
| python3, node/npm, build-essential, jq, curl, … | general dev userland |

It also bakes the `127.0.0.1 localhost` `/etc/hosts` entry Kit's OAuth listener
requires, and creates `/workspace`.

> **Note:** this image intentionally does **not** ship workdir's
> `sandbox-guest-agent` / `sandbox-init`. workdir's custom-image builder injects
> the (static musl) guest agent and init when it converts this OCI image to an
> ext4 rootfs.

## Build locally

```bash
# from the repo root
docker buildx build --platform linux/amd64 \
-f deploy/sandbox/Dockerfile \
-t kit-sandbox .
```

Override pinned versions with `--build-arg` (`GO_VERSION`, `KIT_VERSION`,
`GH_VERSION`, `GLAB_VERSION`, `TEA_VERSION`).

## CI / publishing

`.github/workflows/sandbox-image.yml` builds the image and pushes it to GHCR
(`ghcr.io/mark3labs/kit-sandbox`) on:

- pushes to `master` touching `deploy/sandbox/**`,
- published releases (the image is rebuilt against the released `kit` tag),
- manual `workflow_dispatch` (optionally pinning `kit_version`).

Tags published: `latest` (default branch), `sha-<short>`, branch name, and
`vX.Y.Z` / `vX.Y` on releases.

## Register it as a workdir custom image

Once published, register the OCI image with workdir
([API reference](https://github.com/mv37-org/workdir/blob/main/docs/API.md#images-spec-10-11)).
Pin an **immutable** tag (a release `vX.Y.Z` or a `sha-<short>` tag) rather than
`latest`, so the workdir image definition is reproducible:

```bash
curl -fsSL -X POST https://api.workdir.dev/v1/images \
-H "Authorization: Bearer $WORKDIR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": { "type": "oci",
"image_ref": "ghcr.io/mark3labs/kit-sandbox:v0.82.1" },
"name": "custom/mark3labs/kit-sandbox",
"resources_hint": { "cpu": 2, "memory_mb": 4096, "disk_gb": 16 }
}'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

The build is asynchronous (`202 Accepted`); poll `GET /v1/images/:id` for status
and `build_log`. Then create sandboxes against it:

```jsonc
POST /v1/sandboxes
{ "image": "custom/mark3labs/kit-sandbox",
"image_version": "2026-06-10-ab12cd" }
```

> If the GHCR package is private, grant workdir's builder pull access (make the
> package public, or configure registry credentials on the workdir side).
Loading