This commit is contained in:
Jarek Rozanski 2026-09-27 10:59:40 +02:00
commit 134a7e16ae
No known key found for this signature in database
4 changed files with 210 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.DS_Store
.claude
*.log

119
Containerfile Normal file
View file

@ -0,0 +1,119 @@
# OpenCode dev sandbox: rootless-Podman container with JDK 25, Node 24, pnpm,
# git, curl, and a hash-pinned build of OpenCode (github.com/anomalyco/opencode).
#
# Built as three stages: JDK and Node are pulled from their official upstream
# images and copied (not apt-installed) into a slim Debian final stage, to
# keep the image small while staying on a mainstream glibc distro.
# ---- Stage: JDK source ------------------------------------------------------
# JDK 24 is EOL (Adoptium stopped building it Sept 2025; no 24-jdk* images
# exist). JDK 25 is the current LTS. Built on Ubuntu Noble (glibc 2.39), which
# is older than Debian Trixie's glibc (2.40) below -- copying a binary linked
# against an older glibc into a newer-glibc environment is safe.
FROM eclipse-temurin:25-jdk-noble AS jdk-source
# ---- Stage: Node source ------------------------------------------------------
# Node 24 is the current Active LTS. Matched to the same Debian release as the
# final base so glibc versions are identical -- no compatibility risk at all.
FROM node:24-trixie-slim AS node-source
# ---- Final stage --------------------------------------------------------------
FROM debian:trixie-slim AS final
# Re-declared here: build ARGs don't cross stage boundaries implicitly.
# Auto-populated by podman/buildah from the build host's native platform on a
# plain `podman build` (no --platform needed) for same-arch builds, which
# covers building on either Apple Silicon or Intel Macs. Cross-arch builds
# would need an explicit --platform flag (out of scope for this project).
ARG TARGETARCH
# OpenCode version + per-arch SHA256 pins. Re-verified by hand against the
# actual release assets (curl + sha256sum), not just copied from a summary:
# curl -fsSL -o oc.tar.gz https://github.com/anomalyco/opencode/releases/download/v<ver>/opencode-linux-<x64|arm64>.tar.gz
# sha256sum oc.tar.gz
# Re-run this whenever OPENCODE_VERSION is bumped, and update the two hashes.
ARG OPENCODE_VERSION=1.18.32
ARG OPENCODE_SHA256_AMD64=3046e0404fdc60fb80307e7a47824ba07477364178a4d09baa8548496dd6d43b
ARG OPENCODE_SHA256_ARM64=568461b7d4d8c19865c97e9a1102e613049c6039d01fe772154de873c1865840
# pnpm version, installed via pnpm's own standalone script (not Corepack --
# the pnpm team doesn't recommend Corepack, and Node 25+ dropped bundling it).
ARG PNPM_VERSION=12.6.0
# ca-certificates : TLS roots for curl/git/npm/pnpm/JVM.
# fontconfig,
# libfreetype6 : Temurin's font subsystem needs these even headless.
# libstdc++6 : native runtime needed by libjvm.so and native Node addons.
# git, curl : required tools.
# tar : needed to extract the OpenCode release archive; not
# guaranteed present on a trimmed slim base.
# openssh-client : without it, `git clone git@...` silently fails.
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates \
fontconfig \
libfreetype6 \
libstdc++6 \
git \
curl \
tar \
openssh-client && \
rm -rf /var/lib/apt/lists/*
# --- JDK / Node, copied from the source stages ---------------------------------
COPY --from=jdk-source /opt/java/openjdk /opt/java/openjdk
COPY --from=node-source /usr/local/bin /usr/local/bin
COPY --from=node-source /usr/local/lib/node_modules /usr/local/lib/node_modules
# /usr/local/bin is already on Debian's default PATH, so node/npm/npx need no
# extra PATH entry. JAVA_HOME/bin and PNPM_HOME are non-standard and need one.
ENV JAVA_HOME=/opt/java/openjdk
ENV PNPM_HOME=/opt/pnpm
ENV PATH="${JAVA_HOME}/bin:${PNPM_HOME}/bin:${PATH}"
# Fixed HOME: the container is run with --userns=keep-id, so the runtime UID
# is whatever host user invoked `podman run`, which has no /etc/passwd entry
# inside the image. A plain ENV HOME is unaffected by that UID mapping, so we
# pin it here and make it world-writable (sticky bit, like /tmp) so any mapped
# UID can use it.
ENV HOME=/home/dev
# Belt-and-suspenders on top of OpenCode's default (~/.config/opencode):
# OPENCODE_CONFIG_DIR is OpenCode's one confirmed config-location override.
ENV OPENCODE_CONFIG_DIR=/home/dev/.config/opencode
RUN mkdir -p /home/dev && chmod 1777 /home/dev
# pnpm: standalone installer, pinned version. SHELL must be set explicitly --
# the installer can't infer a shell type inside a minimal `RUN` environment
# with no interactive shell, and errors out (ERR_PNPM_UNKNOWN_SHELL) otherwise.
RUN curl -fsSL https://get.pnpm.io/install.sh | env PNPM_VERSION="${PNPM_VERSION}" SHELL=/bin/bash sh - && \
chmod -R a+rX "${PNPM_HOME}"
# OpenCode: arch-aware fetch, SHA256 verification, install.
# Asset naming uses "x64" for the amd64 arch, not "amd64".
# Confirmed layout: both tarballs contain the binary flat at top level as
# "opencode" (no subdirectory nesting).
RUN set -eu; \
case "$TARGETARCH" in \
amd64) OC_ARCH=x64; OC_SHA256="$OPENCODE_SHA256_AMD64" ;; \
arm64) OC_ARCH=arm64; OC_SHA256="$OPENCODE_SHA256_ARM64" ;; \
*) echo "ERROR: unsupported TARGETARCH='$TARGETARCH'" >&2; exit 1 ;; \
esac; \
curl -fsSL -o /tmp/opencode.tar.gz \
"https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-${OC_ARCH}.tar.gz"; \
printf '%s /tmp/opencode.tar.gz\n' "$OC_SHA256" | sha256sum -c -; \
mkdir -p /tmp/opencode-extract; \
tar -xzf /tmp/opencode.tar.gz -C /tmp/opencode-extract; \
install -m 0755 /tmp/opencode-extract/opencode /usr/local/bin/opencode; \
rm -rf /tmp/opencode.tar.gz /tmp/opencode-extract
# Build-time smoke tests: fail here, not at first `make opencode`.
RUN { ldd /usr/local/bin/opencode || true; } | tee /tmp/opencode-ldd.txt && \
if grep -qi "not found" /tmp/opencode-ldd.txt; then \
echo "ERROR: opencode binary has unresolved shared library dependencies" >&2; \
exit 1; \
fi && \
rm -f /tmp/opencode-ldd.txt
RUN java -version && node --version && pnpm --version && git --version && curl --version
WORKDIR /workspace
CMD ["opencode"]

69
Makefile Normal file
View file

@ -0,0 +1,69 @@
IMAGE_NAME := opencode-sandbox
IMAGE_TAG := local
IMAGE := $(IMAGE_NAME):$(IMAGE_TAG)
NETWORK_NAME := opencode-sandbox-net
WORKDIR_NAME := $(notdir $(patsubst %/,%,$(CURDIR)))
# OpenCode follows the XDG Base Directory spec with three distinct dirs:
# config (settings/plugins), data (auth.json, opencode.db, snapshots), and
# state (session/model history, locks) -- data and state are NOT the same
# directory, despite the similar names.
CONFIG_DIR := $(HOME)/.config/opencode
DATA_DIR := $(HOME)/.local/share/opencode
STATE_DIR := $(HOME)/.local/state/opencode
# No :Z/:z SELinux suffixes: on macOS the host side (APFS) has no SELinux
# concept, so the labeling flag would be a meaningless no-op here.
#
# Known virtiofs caveat (default podman-machine file share on macOS/applehv):
# inotify file-change events don't propagate into the container. Not a
# blocker for this sandbox, just a rough edge if a tool relies on watching
# /workspace for live changes.
RUN_FLAGS := \
--rm -it \
--userns=keep-id \
--network $(NETWORK_NAME) \
--network-alias $(WORKDIR_NAME) \
-v $(CURDIR):/workspace \
-v $(CONFIG_DIR):/home/dev/.config/opencode \
-v $(DATA_DIR):/home/dev/.local/share/opencode \
-v $(STATE_DIR):/home/dev/.local/state/opencode \
-w /workspace
.PHONY: build opencode shell
# Only relevant on macOS, where podman requires a running VM ("podman
# machine"). Handles "machine exists but stopped" -> start it. Does NOT
# handle "no machine exists at all" -- that needs a one-time manual
# `podman machine init`, left as a documented prerequisite since it's a
# heavier, resource-allocating operation.
#
# Written as a single shell line deliberately: macOS ships GNU Make 3.81
# (frozen pre-GPLv3), which predates `.ONESHELL:` (added in 3.82). Without
# it, Make runs a separate shell per newline in the *expanded* recipe text,
# which would split an if/then/fi block from a multi-line variable across
# unrelated shell invocations and break it. A single logical line sidesteps
# the issue entirely and works on any Make version.
define ENSURE_MACHINE
[ "$$(uname -s)" != "Darwin" ] || podman machine list --format '{{.Running}}' 2>/dev/null | grep -q '^true$$' || { echo "==> No running podman machine found; starting default machine..."; podman machine start; }
endef
# Idempotent: creates the shared cross-container network once, reused on
# every subsequent invocation. Any other Podman dev container joined to this
# same network name can resolve and reach this sandbox by its workdir alias.
define ENSURE_NETWORK
podman network exists $(NETWORK_NAME) || podman network create $(NETWORK_NAME)
endef
build:
@$(ENSURE_MACHINE)
podman build -t $(IMAGE) -f Containerfile .
opencode:
@$(ENSURE_MACHINE)
@$(ENSURE_NETWORK)
podman run $(RUN_FLAGS) $(IMAGE) opencode
shell:
@$(ENSURE_MACHINE)
@$(ENSURE_NETWORK)
podman run $(RUN_FLAGS) $(IMAGE) bash

19
SPEC.md Normal file
View file

@ -0,0 +1,19 @@
# OpenCode Container based sandbox
The sandbox creates a rootless Podman based container that runs pinned and cryptographically pinned version of Opencode.
The container launches with current directory as working directory and was read-write permissions.
The container launches with current user's opencode configureation. User is able to modify the opencode config inside the container. All containers share this config so user can switch between workdirs with same config.
The Makefile has 3 targets:
- build - to rebuild containers image
- opencode - starts opencode TUI in container
- shell - opens shell in the container
The container uses small mainstream linux distro (find which is smallest ... Fedora, Ubuntu, etc ... avoid incomplete distros like Alpine)
The container has Java JDK 24, latest LTS Node, pnpm, git, curl, etc installed; use composed images.
The container when running has its own network.
We need to be able to connect this container to other dev containers.
This will be used predominantly on MacOS, so do proper verification of how Podman runs on Mac, SELinux concerns, etc.