Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
92 changes: 92 additions & 0 deletions .github/workflows/sandbox-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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:
- uses: actions/checkout@v4
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

- name: Resolve kit version
id: kitver
run: |
if [ "${{ github.event_name }}" = "release" ]; then
echo "version=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
elif [ -n "${{ inputs.kit_version }}" ]; then
echo "version=${{ inputs.kit_version }}" >> "$GITHUB_OUTPUT"
else
echo "version=latest" >> "$GITHUB_OUTPUT"
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- uses: docker/setup-buildx-action@v3

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

- name: Docker metadata (tags + labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=sha,format=short
type=semver,pattern={{version}},event=release
type=semver,pattern={{major}}.{{minor}},event=release
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

- name: Build and push
uses: docker/build-push-action@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
100 changes: 100 additions & 0 deletions deploy/sandbox/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# 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 (multi-arch capable; workdir runs x86_64 Firecracker):
# docker buildx build --platform linux/amd64 -f deploy/sandbox/Dockerfile -t kit-sandbox .
#
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive

# Pinned tool versions — override at build time with --build-arg.
ARG GO_VERSION=1.26.4
ARG KIT_VERSION=latest
ARG GH_VERSION=2.95.0
ARG GLAB_VERSION=1.105.0
ARG TEA_VERSION=0.14.1

# Resolved automatically by buildx (linux/amd64 -> amd64, linux/arm64 -> arm64).
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 \
&& 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 || true
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# 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 \
&& 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 \
&& 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 /usr/local/bin/tea \
&& chmod +x /usr/local/bin/tea
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# 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.
RUN set -e; \
go version; \
kit --version || true; \
gh --version; \
glab --version; \
tea --version

CMD ["/bin/bash"]
82 changes: 82 additions & 0 deletions deploy/sandbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 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)):

```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:latest" },
"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