diff --git a/.github/workflows/sandbox-image.yml b/.github/workflows/sandbox-image.yml new file mode 100644 index 00000000..5b7d705d --- /dev/null +++ b/.github/workflows/sandbox-image.yml @@ -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 + # 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 diff --git a/deploy/sandbox/Dockerfile b/deploy/sandbox/Dockerfile new file mode 100644 index 00000000..87a4fcdd --- /dev/null +++ b/deploy/sandbox/Dockerfile @@ -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__checksums.txt, +# glab checksums.txt, tea--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"] diff --git a/deploy/sandbox/README.md b/deploy/sandbox/README.md new file mode 100644 index 00000000..e81e3cff --- /dev/null +++ b/deploy/sandbox/README.md @@ -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-`, 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-` 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 } + }' +``` + +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).