Rewrite DOCKER_HUB.md as a hand-maintained slim template
Validate / docs-check (push) Successful in 16s
Validate / validate-base (push) Successful in 11m16s
Validate / validate-omos (push) Failing after 18m33s
Validate / validate-with-pi (push) Successful in 13m46s
Validate / validate-omos-with-pi (push) Failing after 19m52s

The previous derive-from-README mechanism (split_sections, SECTION_RULES,
TRIM_SUBSECTIONS, REPLACEMENTS) generated a 24 997 byte Hub doc with
3 byte headroom against the 25 kB Hub limit. Every README addition
forced a 'trim something else first' exercise, and the resulting copy
was awkward (terse, repetitive linkbacks injected mid-section).

Replace with a single hand-maintained HUB_TEMPLATE constant. The Hub
doc is now intentionally slim (~5.5 kB, ~78 percent headroom) and
focuses on what Hub readers actually need: elevator pitch, image
variants, quick start, what's inside, auth, persistence, and link-outs
to the gitea README for depth.

Trade-off: when image-variants or quick-start change, update
HUB_TEMPLATE here too. That coupling is now explicit and local rather
than spread across SECTION_RULES + REPLACEMENTS + TRIM machinery,
and most README edits no longer require regenerating DOCKER_HUB.md
at all.

Generator simplified from 323 lines to 199 lines (270-line net
reduction across the script + DOCKER_HUB.md). README and Hub doc are
now independent surfaces.

CHANGELOG and AGENTS updated to reflect the new coupling. Release-day
checklist tightened: README -> regenerate DOCKER_HUB ONLY if
HUB_TEMPLATE changed -> promote CHANGELOG -> grep AGENTS -> commit
-> tag.
This commit is contained in:
2026-05-09 15:49:43 +02:00
parent 9df126c7a9
commit f86c4b18cf
4 changed files with 133 additions and 741 deletions
+5 -5
View File
@@ -11,9 +11,9 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
- `entrypoint-user.sh` — runs as developer: git config, opencode.jsonc generation (delegated to `generate-config.py`), pi-toolkit + pi-extensions deploy (when pi installed), pi settings.json bootstrap, mempalace pi-bridge symlink, skillset auto-deploy from mounted skillset repo, OMOS setup.
- `rootfs/usr/local/lib/opencode-devbox/generate-config.py` — generates `~/.config/opencode/opencode.jsonc` from env vars. Never overwrites an existing config (checks both `.json` and `.jsonc`). Auto-registers MCP servers for detected tools (mempalace via `mempalace-mcp`, gitea-mcp, context7 remote endpoint).
- `scripts/smoke-test.sh` — post-build image verification. Asserts binary presence, opencode startup, entrypoint correctness, config generation idempotency, and image size thresholds. Used by both CI workflows.
- `scripts/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from `README.md` using explicit section rules. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow).
- `DOCKER_HUB.md`**auto-generated** from README. Do not edit directly. Pushed to Docker Hub description via CI API call. Must stay under 25 kB. Short description field must be ≤100 bytes.
- `README.md` — authoritative source documentation. Sections are selected/dropped/replaced for DOCKER_HUB.md per `SECTION_RULES` in `scripts/generate-dockerhub-md.py`.
- `scripts/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from a hand-maintained `HUB_TEMPLATE` constant. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow).
- `DOCKER_HUB.md`**auto-generated** from `HUB_TEMPLATE` in `scripts/generate-dockerhub-md.py`. Do not edit directly. Pushed to Docker Hub description via CI API call. Must stay under 25 kB. Short description field must be ≤100 bytes.
- `README.md` — authoritative source documentation for everything in this repo. Independent of `DOCKER_HUB.md`: the Hub doc is hand-maintained in the generator's `HUB_TEMPLATE` and intentionally slim, linking back to the gitea README for depth.
- `.gitea/workflows/validate.yml` — lightweight amd64 build + smoke test on push to main and PRs. Also runs the DOCKER_HUB.md sync check.
- `.gitea/workflows/docker-publish.yml` — CI pipeline on tag push: smoke-test each variant on amd64, then full multi-arch (amd64 + arm64) build-and-push, then update Docker Hub description.
@@ -35,12 +35,12 @@ When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile`
- **entrypoint.sh volume ownership loop** — when adding a new named volume mount point, add it to the `for dir in ...` loop in `entrypoint.sh` so root-owned volumes get chowned on startup. The loop writes a `.devbox-owner` sentinel after a successful chown so subsequent starts skip the recursive walk. Users should not touch these files.
- **Documentation coupling on release** — four docs co-vary and drift in lockstep when not updated together:
- `README.md` is the source of truth for user-facing build/run/config detail.
- `DOCKER_HUB.md` is auto-generated by `scripts/generate-dockerhub-md.py` from README. CI's `--check` run fails if it's stale or any new top-level README section is missing from `SECTION_RULES`. Hard cap: 25 kB Hub limit (current: ~24.9 kB, very tight — trim before adding).
- `DOCKER_HUB.md` is auto-generated from `HUB_TEMPLATE` in `scripts/generate-dockerhub-md.py`. CI's `--check` run fails if it's stale. Hub-facing copy is intentionally slim (~5.5 kB, ~78% headroom against the 25 kB Hub limit) — update the template here when image variants, quick-start flow, or the elevator pitch change. README.md no longer feeds into Hub, so README edits do NOT require regenerating DOCKER_HUB.md.
- `CHANGELOG.md` records every release. When cutting a tag, **promote `## Unreleased` to `## vX.Y.Z[n] — YYYY-MM-DD` BEFORE pushing the tag** so the tag points at a CHANGELOG that names itself. Keep entries reverse-chronological (newest at top, after the `Unreleased` block). Doc-only updates that happen post-tag (Hub description live-patches, README clarifications) get a fresh `## Unreleased` block with a note that they don't trigger a new image build.
- `AGENTS.md` (this file) carries domain facts that change on structural releases — tag-count statements, CI job lists, install contracts. After any change to `.gitea/workflows/*.yml` or the variant matrix, grep this file for stale numbers (`grep -nE "four|eight|all [0-9]"`).
- `.env.example` must be hand-updated to match Dockerfile/entrypoint behavior — it is not auto-generated.
Release-day checklist: README → regenerate DOCKER_HUB.md → promote CHANGELOG Unreleased → grep AGENTS.md for stale counts → commit → tag → push tag.
Release-day checklist: README → (regenerate DOCKER_HUB.md only if HUB_TEMPLATE changed) → promote CHANGELOG Unreleased → grep AGENTS.md for stale counts → commit → tag → push tag.
- **GitHub/Gitea-sourced binaries float by default** — gosu, fzf, git-lfs, nvim, bat, eza, zoxide, uv, gitea-mcp, Go, oh-my-opencode-slim all default to `latest`. Each build-time install step reads the `/releases/latest` Location redirect (or the go.dev JSON feed for Go) and derives the concrete version. Use the same `ARCH` case-switch pattern for multi-arch support (amd64/arm64). Intentional pins: `OPENCODE_VERSION` (drives the image tag), `NODE_VERSION=22` (major pin), `DEBIAN_VERSION=trixie-slim` (OS base). Adding a new upstream tool: follow the existing floated-version pattern, don't hardcode a specific tag.
- **Resolved versions are logged by the smoke test** — `scripts/smoke-test.sh` prints a "Resolved component versions" table as its first step. CI logs always capture what got baked into a given image even when ARGs default to `latest`.
- **Shell scripts use `set -euo pipefail`** — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with `|| true`.
+5
View File
@@ -19,6 +19,11 @@ Image changes (will ship in the next tagged release):
- **Fix:** `pi install npm:<pkg>` (and any `npm install -g`) by the `developer` user no longer EACCES against the system npm prefix. `NPM_CONFIG_PREFIX` is now `/home/developer/.pi/npm-global` and the prefix's `bin/` is prepended to `PATH`. The directory lives on the `devbox-pi-config` named volume, so user-installed pi packages (themes, skills, extensions) survive container recreation and image rebuilds. Build-time `npm install -g` calls (opencode, pi, oh-my-opencode-slim) are unaffected because the new ENVs are declared after those steps in the Dockerfile, so the baked binaries still install to `/usr` and are not shadowed by the volume mount.
Docs-only (the DOCKER_HUB.md change can be patched live to Hub without a CI rebuild; AGENTS.md change is repo-internal):
- **Hub doc rewrite:** `DOCKER_HUB.md` is now generated from a hand-maintained `HUB_TEMPLATE` constant in `scripts/generate-dockerhub-md.py` instead of a section-by-section transformation of `README.md`. Drops from 24 997 bytes (3 byte headroom) to 5 551 bytes (~78% headroom). The old derive-from-README mechanism (`SECTION_RULES`, `TRIM_SUBSECTIONS`, `REPLACEMENTS`, `split_sections`, `trim_subsections`) is gone — README and Hub doc are now independent surfaces. Hub copy stays slim and links out to the gitea README for full depth (build args, multi-user setup, AWS Bedrock walkthrough, MemPalace deep-dive, language-specific dev sections). Trade-off: image-variants table and quick-start flow are now coupled to `HUB_TEMPLATE` and need a manual edit when they change — explicit and local rather than spread across rules.
- **AGENTS.md:** "Documentation coupling on release" rule updated — README edits no longer require regenerating DOCKER_HUB.md. Release-day checklist tightened.
## v1.14.41b — 2026-05-08
**Optional pi as second harness.**
+39 -540
View File
@@ -1,10 +1,10 @@
# opencode-devbox — Docker Hub
# opencode-devbox
Portable AI developer environment for [opencode](https://opencode.ai). Debian-based, with git, SSH, Node.js, AWS CLI v2, and common dev tools pre-installed.
## Image Variants
Designed for teams who want a reproducible coding-agent setup that runs the same on every laptop and CI runner — without forcing each developer to install Bun, Node, AWS CLI, mempalace, or maintain shell config drift across machines.
Four image variants are published for each release:
## Image Variants
| Tag | Description |
|---|---|
@@ -15,8 +15,6 @@ Four image variants are published for each release:
All variants support `linux/amd64` and `linux/arm64`.
> **NOTE:** This file is auto-generated from `README.md` by `scripts/generate-dockerhub-md.py`. Edit README.md and regenerate rather than editing this file directly.
## Quick Start
```bash
@@ -30,9 +28,9 @@ docker run -it --rm \
joakimp/opencode-devbox:latest
```
This drops you straight into opencode with your project mounted at `/workspace`.
Drops you straight into opencode with your project mounted at `/workspace`.
For an interactive shell first (useful for AWS SSO login):
For an interactive shell first (useful for AWS SSO login, multi-harness workflows, or just `bash`):
```bash
docker run -it --rm \
@@ -43,554 +41,55 @@ docker run -it --rm \
joakimp/opencode-devbox:latest bash
```
Then run `opencode` when ready.
Then run `opencode`, `pi` (on `*-with-pi` variants), or `aws sso login` from the shell.
For docker-compose users, see the source repo for `docker-compose.yml` and `.env.example` templates.
For docker-compose users, the source repo provides `docker-compose.yml`, `.env.example`, and a one-liner `docker compose up -d` workflow with named volumes pre-wired.
## Features
## What's Inside
- **Debian trixie** base — glibc, full PTY/terminal support
- **Configurable providers** — Anthropic, OpenAI, AWS Bedrock via env vars
- **Host filesystem access** — bind mount any directory as `/workspace`
- **SSH key forwarding** — git push/pull to private repos
- **MCP server support** — Node.js included for `npx`-based MCP servers
- **Non-root user** — runs as `developer` with UID auto-matched to workspace owner (sudo available)
- **Python via uv** — `uv` package manager included; install Python on demand with `uv python install`
- **Rust via rustup** — `rustup-init` included; bootstrap Rust on demand with `rustup-init -y`
- **Optional runtimes** — Python (apt), Go via build args (Node.js always included — required for opencode v1.x)
- **Multi-agent orchestration** — optional [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) integration via build arg
- **AWS CLI v2** — built-in SSO/Bedrock authentication with headless device-code flow
- **Multi-arch** — amd64 and arm64
- **[opencode](https://opencode.ai)** — primary coding-agent harness. Multi-provider (Anthropic, OpenAI, Bedrock, Google, Groq, etc.).
- **[pi](https://github.com/mariozechner/pi-coding-agent)** *(in `*-with-pi` variants)* — lightweight TUI coding-agent that coexists with opencode and shares the same mempalace install. Includes the `mcp-loader` extension so any local-stdio or remote streamable-HTTP MCP server (searxng, gitea, context7, …) can be added by editing `~/.pi/agent/settings.json`.
- **[mempalace](https://github.com/MemPalace/mempalace)** — persistent AI memory layer (ChromaDB + SQLite). Wing/diary/knowledge-graph entries are mutually visible to opencode and pi.
- **[oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim)** *(in `*-omos` variants)* — multi-agent orchestration on top of opencode (council, fallback chains, named agents).
- **AWS CLI v2** with SSO support, **Node.js LTS**, **Bun** (OMOS variants), **uv** (Python), **gosu** for clean UID/GID adjustment to match your host workspace.
- **MCP wrappers** for mempalace pre-installed and pre-wired to both harnesses.
## Usage
## Authentication
### Prerequisites
The container reads provider credentials from environment variables and host-mounted config:
Bind-mounted directories must exist on the host before starting the container. Docker creates missing directories as root-owned, which causes permission issues.
- **Anthropic / OpenAI / Groq / others:** set `OPENCODE_PROVIDER` and the corresponding `*_API_KEY` via `-e` or `.env`.
- **AWS Bedrock (SSO):** mount `~/.aws` from the host, `OPENCODE_PROVIDER=amazon-bedrock`, then `aws sso login` inside the container. Tokens persist across container restarts via the host bind-mount.
- **OAuth / device-code providers:** auth state lives in opencode's config, which is persisted via the `devbox-opencode-config` named volume.
```bash
# Required: workspace for your projects
mkdir -p ~/projects
```
Full Bedrock walkthrough (IAM roles, permissions, multi-account setups): see the [AWS Bedrock Authentication](
https://gitea.jordbo.se/joakimp/opencode-devbox#aws-bedrock-authentication
) section on gitea.
### Connecting to the container
## Persistence
From your laptop, SSH into the remote server where Docker is running, then start the container:
```bash
# 1. SSH into the remote server
ssh user@remote-server
# 2. Navigate to the project
cd opencode-devbox
# 3. Start the container with an interactive shell
docker compose run --rm devbox bash
# You're now inside the container — run commands here
aws sso login --sso-session <your-sso-session> --use-device-code
opencode
```
### Running modes
**Interactive shell** — enter the container, run multiple commands:
```bash
docker compose run --rm devbox bash
```
**Direct to opencode** — skips the shell, launches opencode immediately:
```bash
docker compose run --rm devbox
```
**Background container** — keep it running, attach when needed:
```bash
# Start in background
docker compose up -d
# Attach a shell to the running container
docker compose exec -u developer devbox bash
# Or run a single command inside it
docker compose exec -u developer devbox aws --version
```
> `run` creates a new container (cleaned up with `--rm`). `exec` attaches to an already running one.
## Configuration
### Environment Variables
| Variable | Description | Default |
| Volume | Mount | Survives |
|---|---|---|
| `OPENCODE_PROVIDER` | LLM provider (`anthropic`, `openai`, `amazon-bedrock`) | `anthropic` |
| `OPENCODE_MODEL` | Model override | Provider default |
| `ANTHROPIC_API_KEY` | Anthropic API key | — |
| `OPENAI_API_KEY` | OpenAI API key | — |
| `AWS_REGION` | AWS region for Bedrock | `us-east-1` |
| `AWS_PROFILE` | AWS SSO profile name | `default` |
| `GIT_USER_NAME` | Git commit author name | — |
| `GIT_USER_EMAIL` | Git commit author email | — |
| `WORKSPACE_PATH` | Host path to mount | `.` |
| `SSH_KEY_PATH` | Host SSH key directory | `~/.ssh` |
| `USER_UID` | Override container user UID | Auto-detect from `/workspace` |
| `USER_GID` | Override container user GID | Auto-detect from `/workspace` |
| `LANG` | System locale | `en_US.UTF-8` |
| `LANGUAGE` | Language priority list | `en_US:en` |
| `LC_ALL` | Override all locale settings | `en_US.UTF-8` |
| `EDITOR` | Default text editor | `nvim` |
| `ENABLE_OMOS` | Enable oh-my-opencode-slim multi-agent orchestration | `false` |
| `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` |
| `OMOS_SKILLS` | Install OMOS recommended skills on first run | `true` |
| `OMOS_RESET` | Force regenerate OMOS config on next start | `false` |
| `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect |
| `devbox-opencode-config` | `~/.config/opencode` | container recreate, image rebuild |
| `devbox-pi-config` | `~/.pi` | container recreate, image rebuild — incl. user-installed pi packages via `pi install` (`NPM_CONFIG_PREFIX` points into the volume) |
| `devbox-palace` (uncomment) | `~/.mempalace` | container recreate, image rebuild — palace data is precious, treat as primary storage |
| `devbox-chroma-cache` | `~/.cache/chroma` | container recreate (model cache, disposable — re-downloads in seconds) |
### Custom opencode config
Workspace bind-mount (`/workspace`) is your project directory on the host, so source code is never inside the container.
Opencode configuration is persisted automatically via the named volume `devbox-opencode-config`. This volume is mounted at `/home/developer/.config/opencode` by default — no host directory setup required. All changes to `opencode.jsonc`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json` survive container recreation.
Full persistence reference, including multi-user (`SIGNUM`) isolation and host bind-mount alternatives: see the [README on gitea](https://gitea.jordbo.se/joakimp/opencode-devbox#persistence).
When an existing `opencode.jsonc` is found in the volume, the `OPENCODE_PROVIDER` auto-config is skipped.
## Where to Go Next
**Alternative: host bind-mount** — if you specifically want to share config from the host (e.g. to version-control it or sync across machines), replace the named volume with a bind mount:
- **Full README** with build args, every feature in detail, troubleshooting: <https://gitea.jordbo.se/joakimp/opencode-devbox>
- **CHANGELOG** for version history: <https://gitea.jordbo.se/joakimp/opencode-devbox/src/branch/main/CHANGELOG.md>
- **Issues / source / docker-compose templates:** <https://gitea.jordbo.se/joakimp/opencode-devbox>
- **Agent-facing internals** (for future maintainers / coding agents working in the repo): <https://gitea.jordbo.se/joakimp/opencode-devbox/src/branch/main/AGENTS.md>
```yaml
volumes:
- ~/.config/opencode:/home/developer/.config/opencode
```
## License
> **Portability note:** The mounted config runs inside a Linux container. Any absolute paths inside `opencode.jsonc` (for example, host-specific `plugin` entries like `file:///usr/local/lib/node_modules/...` or `file:///opt/homebrew/...`) will not resolve inside the container. Prefer bare package specifiers (e.g. `"oh-my-opencode-slim"`) that resolve via `node_modules` lookup, which works on both macOS and Linux hosts.
MIT. See <https://gitea.jordbo.se/joakimp/opencode-devbox/src/branch/main/LICENSE>.
### Custom skills
---
Skills are deployed automatically from a skillset repo on container start. The entrypoint detects the skillset location in this order:
1. `SKILLSET_CONTAINER_PATH` env var (explicit path to skillset repo inside container)
2. `~/skillset` mount (if present)
3. `/workspace/skillset` fallback (if your workspace contains a `skillset/` directory)
When a skillset repo is detected, its skills are symlinked into `~/.agents/skills/` automatically. No manual configuration needed.
> **Warning:** Do not bind-mount a host `~/.agents/skills` directory directly into the container. This conflicts with the symlink-based auto-deploy mechanism and causes broken skill references.
### Neovim configuration
The image includes neovim 0.12 with `EDITOR=nvim` set by default. To use your own neovim config (and have plugins auto-install via lazy.nvim on first start), mount it from the host:
```yaml
volumes:
- ~/.config/nvim:/home/developer/.config/nvim:ro
```
### Python development with uv
The image includes Python 3.13 (from Debian Trixie) and [uv](https://docs.astral.sh/uv/), a fast Python package manager that replaces pip, venv, and pyenv:
```bash
# Python 3.13 is available out of the box
python3 --version
# Use uv for package management
uv venv
uv pip install -r requirements.txt
# Or use uv's project workflow (reads pyproject.toml)
uv sync
# Run a Python script
uv run python script.py
# Install standalone Python tools
uvx ruff check .
# Install a newer Python version (persists with devbox-uv volume)
uv python install 3.14
```
Python installations are stored in `~/.local/share/uv/`. To persist them across container restarts, add the `devbox-uv` named volume to your `docker-compose.yml`:
```yaml
volumes:
- devbox-uv:/home/developer/.local/share/uv
volumes:
devbox-uv:
```
Project virtual environments (`.venv`) are stored in your workspace directory and persist automatically via the `/workspace` bind mount.
### Rust development with rustup
The image includes `rustup-init`, the Rust toolchain installer. Rust is not pre-installed but can be bootstrapped on demand:
```bash
# One-time setup: install Rust toolchain (~300MB, persists with volumes)
rustup-init -y
source ~/.cargo/env
# Now use Rust normally
cargo new my-project
cargo build
cargo run
```
To persist Rust toolchains and cargo data across container restarts, add named volumes to your `docker-compose.yml`:
```yaml
volumes:
- devbox-rustup:/home/developer/.rustup
- devbox-cargo:/home/developer/.cargo
volumes:
devbox-rustup:
devbox-cargo:
```
### JavaScript and TypeScript
The base image includes **Node.js 22** and **npm** — sufficient for most JavaScript and TypeScript development:
```bash
# Initialize a new project
npm init -y
# Install dependencies
npm install
# Run TypeScript (via tsx, ts-node, etc.)
npx tsx src/index.ts
# Use npx for one-off tools
npx tsc --init
```
The OMOS image variant also includes **Bun**, a faster JavaScript runtime and package manager:
```bash
bun init
bun install
bun run src/index.ts
```
Node modules are stored in your project directory under `/workspace` and persist automatically.
### VS Code integration
VS Code can connect directly to a running opencode-devbox container for a full IDE experience with IntelliSense, debugging, and extensions running inside the container.
**Local Docker (Docker running on your workstation):**
1. Install the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension
2. Start the container: `docker compose up -d`
3. In VS Code: `Ctrl+Shift+P` → "Dev Containers: Attach to Running Container" → select `opencode-devbox`
**Remote Docker (Docker running on a remote server, e.g. via SSH):**
1. Install the [Remote - SSH](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh) and [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extensions
2. Connect to the remote host: `Ctrl+Shift+P` → "Remote-SSH: Connect to Host"
3. On the remote host, start the container: `docker compose up -d`
4. In VS Code (now connected to the remote): `Ctrl+Shift+P` → "Dev Containers: Attach to Running Container"
VS Code extensions installed inside the container persist as long as the container exists (not removed with `docker compose down`). For persistent extension storage across container recreations, add a named volume:
```yaml
volumes:
- devbox-vscode:/home/developer/.vscode-server
```
## oh-my-opencode-slim (Multi-Agent Orchestration)
[oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) adds a multi-agent layer on top of opencode — an Orchestrator delegates tasks to specialized agents (Explorer, Oracle, Librarian, Designer, Fixer), each configurable with different models and providers.
### Setup
A pre-built OMOS image is available on Docker Hub as `joakimp/opencode-devbox:latest-omos`. Alternatively, build from source:
**1. Build the image with OMOS support:**
```bash
docker compose build --build-arg INSTALL_OMOS=true
```
This installs Bun and the oh-my-opencode-slim package into the image.
**2. Enable in `.env`:**
```bash
ENABLE_OMOS=true
```
**3. Run as normal:**
```bash
docker compose run --rm devbox
```
On first start, the entrypoint runs the oh-my-opencode-slim installer in non-interactive mode. It generates agent configuration at `~/.config/opencode/oh-my-opencode-slim.json` inside the container. The default preset uses OpenAI models — edit the generated config or mount your own to customize.
### OMOS Environment Variables
| Variable | Default | Description |
|---|---|---|
| `ENABLE_OMOS` | `false` | Activate oh-my-opencode-slim on container start |
| `OMOS_TMUX` | `false` | Enable tmux pane integration (tmux is included in the base image) |
| `OMOS_SKILLS` | `true` | Install recommended skills (simplify, agent-browser, cartography) |
| `OMOS_RESET` | `false` | Force regenerate config on next start (backs up existing config) |
### Custom Configuration
If you mount the opencode config directory (see Custom opencode config above), the `oh-my-opencode-slim.json` file is included and persists across restarts. Edit it directly to control which models power each agent, fallback chains, council setup, and more.
See the [oh-my-opencode-slim configuration docs](https://github.com/alvinunreal/oh-my-opencode-slim/blob/master/docs/configuration.md) for the full reference.
### Verifying Agents
After starting opencode with OMOS enabled, run inside the opencode session:
```
ping all agents
```
All six agents should respond if your provider authentication is working.
## pi (alternative/complementary harness)
[pi](https://github.com/mariozechner/pi-coding-agent) is a lightweight TUI coding-agent that can run alongside opencode in the same container. Both harnesses share the same mempalace install and palace data — wing/diary entries created by one are visible to the other. Available on the `*-with-pi` and `*-omos-with-pi` image tags.
### Run
The default container CMD is `bash -l`, so `compose run --rm devbox` drops to a login shell. From there pick a harness:
```bash
docker compose run --rm devbox # bash — then run `pi`, `opencode`, or `aws sso login`
docker compose run --rm devbox pi # launch pi directly
docker compose exec -u developer devbox pi # attach into a `compose up -d` container
```
### MemPalace integration
pi ships pre-wired with the mempalace bridge — the `mempalace.ts` extension is symlinked into `~/.pi/agent/extensions/` at first start, exposing palace MCP tools (search, diary, knowledge graph) to pi natively. The same `~/.mempalace/` palace is shared with opencode, so memories are mutually visible across harnesses. To persist palace data across container recreate, uncomment the `devbox-palace` volume in `docker-compose.yml` (see *MemPalace* section below).
### Persistence
| Path in container | Volume | Contains |
|---|---|---|
| `/home/developer/.pi` | `devbox-pi-config` (default) | `settings.json`, `agent/extensions/`, `auth.json`, `sessions/`, plus `git/` and `npm-global/` (`NPM_CONFIG_PREFIX` points here) so `pi install npm:` / `git:` and `npm i -g` survive container recreate and image rebuild. |
| `/home/developer/.mempalace` | `devbox-palace` (uncomment) | Shared palace — visible to pi and opencode |
Baked pi (`/usr/bin/pi`, `/opt/pi-*`) ships in the image; rebuild to upgrade. `npm i -g @mariozechner/pi-coding-agent` lands on the volume and wins via `PATH`.
Full build args, extension list, and toolkit detail: <https://gitea.jordbo.se/joakimp/opencode-devbox#pi-alternativecomplementary-harness>
## AWS Bedrock Authentication
When using AWS Bedrock as your LLM provider, you need:
### 1. AWS config on the host
The container needs access to your `~/.aws/config` with SSO session configuration. If you already have this on another machine, copy it:
```bash
scp -r user@other-machine:~/.aws ~/.aws
```
Or configure from scratch on the host:
```bash
aws configure sso
```
### 2. Mount `~/.aws` into the container
Uncomment the AWS volume mount in `docker-compose.yml`:
```yaml
- ~/.aws:/home/developer/.aws
```
Note: do **not** use `:ro` — SSO writes token cache files to this directory.
### 3. Authenticate inside the container
Since the container runs headless (no browser), use the device-code flow:
```bash
# Start the container
docker compose up -d
docker compose exec -u developer devbox bash
# Authenticate — prints a URL and code you open in your local browser
aws sso login --sso-session <your-sso-session> --use-device-code
# Once approved in the browser, start opencode
opencode
```
The `--use-device-code` flag outputs a URL and short code instead of trying to open a browser. Copy the URL into any browser (on your laptop, phone, etc.), enter the code, and complete the 2FA flow. The CLI in the container picks up the session automatically.
SSO sessions typically last 812 hours before requiring re-authentication. Since `~/.aws` is mounted from the host, tokens persist across container restarts.
## MemPalace — persistent AI memory
The image includes [MemPalace](https://github.com/MemPalace/mempalace), a local-first AI memory system that stores conversation history verbatim and retrieves it via semantic search. Nothing leaves your machine.
> MemPalace adds ~300 MB to the image (chromadb, embedding model deps). If you don't use it, rebuild with `--build-arg INSTALL_MEMPALACE=false` to shrink the image.
### Enabling persistence
Uncomment the palace volume in `docker-compose.yml`:
```yaml
- devbox-palace:/home/developer/.mempalace
```
Without the volume, palace data lives in the container's writable layer and is lost on `--force-recreate`.
### MCP integration with opencode
Add mempalace as an MCP server in your `opencode.jsonc` (inside `~/.config/opencode/`):
```json
{
"mcp": {
"mempalace": {
"type": "local",
"command": ["mempalace-mcp"]
}
}
}
```
> The image installs mempalace into an isolated `uv tool` venv at `/opt/uv-tools/mempalace/`. `uv tool install` places `mempalace-mcp` on `PATH` as a shim whose shebang points at the venv's Python, so MCP clients can invoke it as a normal binary without worrying about the venv. Do **not** use `["python3", "-m", "mempalace.mcp_server"]` — the system Python cannot import from the uv-managed venv and you'll get `ModuleNotFoundError` / `MCP error -32000: connection closed`.
This gives opencode access to 29 MCP tools for searching memory, querying the knowledge graph, managing wings/rooms/drawers, and agent diaries.
### Basic usage
```bash
# Mine project files into the palace
mempalace mine /workspace
# Mine conversation transcripts
mempalace mine ~/.local/share/opencode/ --mode convos
# Search memory
mempalace search "why did we switch to eno1"
# Load context for a new session
mempalace wake-up
```
Each workspace gets its own isolated "wing" — memories never leak between projects.
### Scheduled mining (mempalace-toolkit)
The image bakes in [mempalace-toolkit](https://gitea.jordbo.se/joakimp/mempalace-toolkit), a small set of bash wrappers that pair with mempalace for two common routines:
```bash
# Mine opencode session history (reads ~/.local/share/opencode/opencode.db, stages JSONL, mines into wing_conversations)
mempalace-session
# Mine a project's docs into a dedicated wing
mempalace-docs /workspace/my-project
```
Both wrappers are idempotent and dedup-aware — re-running them on unchanged input is a cheap no-op.
For weekly automated runs, the toolkit ships ready-to-use scheduler templates (systemd user timer, launchd user agent, cron) in its [`contrib/`](https://gitea.jordbo.se/joakimp/mempalace-toolkit/src/branch/main/contrib) directory. The `*-devbox` variants are designed for this container: host-side schedulers that `docker exec` into the running opencode-devbox.
Disable the toolkit (keeps mempalace itself) with `--build-arg INSTALL_MEMPALACE_TOOLKIT=false`. Pin to a specific ref with `--build-arg MEMPALACE_TOOLKIT_REF=v0.3.0` once tagged releases exist.
### Storage
Two separate named volumes keep different data classes apart:
- **Palace data** (`~/.mempalace/`): ChromaDB vectors, SQLite knowledge graph, drawers. This is your memory — back it up, treat it as precious. Persists via the `devbox-palace` named volume.
- **Embedding model cache** (`~/.cache/chroma/`): ONNX model (~79 MB), downloaded automatically on first search. Disposable — blow it away and it re-downloads in ~4 seconds. Persists via the `devbox-chroma-cache` named volume so you don't re-download on every container recreation.
- **No API keys required** for core functionality (local embeddings via ONNX).
Both volumes are commented out by default in `docker-compose.yml` — uncomment to enable:
```yaml
- devbox-palace:/home/developer/.mempalace
- devbox-chroma-cache:/home/developer/.cache/chroma
```
**Air-gapped environments:** pre-populate the `devbox-chroma-cache` volume with the `all-MiniLM-L6-v2/` model contents. The palace volume needs no pre-population.
## Gitea MCP server
The image includes the [official Gitea MCP server](https://gitea.com/gitea/gitea-mcp) (`gitea-mcp`), providing 50+ MCP tools for interacting with self-hosted Gitea instances — repositories, issues, pull requests, releases, branches, wiki, and Actions.
### Setup
1. Create a Personal Access Token on your Gitea instance (Settings → Applications → Generate Token, scopes: `repo`, `read:user`).
2. Add to your `.env`:
```env
GITEA_HOST=https://your-gitea-instance.example.com
GITEA_ACCESS_TOKEN=your_token_here
```
3. Enable the gitea MCP server in your `opencode.jsonc`:
```json
{
"mcp": {
"gitea": {
"type": "local",
"command": ["gitea-mcp", "-t", "stdio", "--host", "{env:GITEA_HOST}"],
"environment": {
"GITEA_ACCESS_TOKEN": "{env:GITEA_ACCESS_TOKEN}"
},
"enabled": true
}
}
}
```
The server is installed but disabled by default — it requires authentication to be useful.
## Architecture
```
Host Machine
├── ~/projects/my-app ──bind mount──▶ /workspace (container)
├── ~/.ssh ──bind mount──▶ /home/developer/.ssh (ro)
├── ~/.aws ──bind mount──▶ /home/developer/.aws (Bedrock SSO)
└── .env ──env vars───▶ provider config + API keys
Container (Debian trixie)
├── opencode binary
├── oh-my-opencode-slim (optional — multi-agent orchestration plugin, includes Bun)
├── AWS CLI v2 (SSO + Bedrock auth)
├── neovim 0.12, tmux, htop, bat, eza, zoxide, uv, rustup, make, gcc, g++, rsync
├── git, git-crypt, age, ssh, ripgrep, fd, fzf, jq, curl, tree
├── Node.js (for MCP servers)
├── Bun (optional — included with oh-my-opencode-slim)
├── entrypoint.sh (UID adjustment, git config, provider setup)
└── /workspace ← your code lives here
```
### Data persistence
| Path in container | Source | Survives `--rm`? | Contains |
|---|---|---|---|
| `/workspace` | Host bind mount | ✅ Yes | Your project files |
| `/home/developer/.ssh` | Host bind mount (ro) | ✅ Yes | SSH keys |
| `/home/developer/.aws` | Host bind mount (if configured) | ✅ Yes | AWS credentials/SSO cache |
| `/home/developer/.local/share/opencode` | Named volume `devbox-data` | ✅ Yes | Session history, memory |
| `/home/developer/.local/state/opencode` | Named volume `devbox-state` | ✅ Yes | TUI settings (theme, toggles) |
| `/home/developer/.cache/bash` | Named volume `devbox-shell-history` | ✅ Yes | Bash history (`$HISTFILE`), survives container recreate |
| `/home/developer/.local/share/zoxide` | Named volume `devbox-zoxide` | ✅ Yes | Zoxide directory history (`z <fragment>` jump targets) |
| `/home/developer/.local/share/nvim` | Named volume `devbox-nvim-data` | ✅ Yes | Neovim plugins, Mason LSP installs, Lazy plugin cache |
| `/home/developer/.local/share/uv` | Named volume `devbox-uv` (if configured) | ✅ Yes | Python installs, uv tool installs |
| `/home/developer/.rustup` | Named volume `devbox-rustup` (if configured) | ✅ Yes | Rust toolchains |
| `/home/developer/.cargo` | Named volume `devbox-cargo` (if configured) | ✅ Yes | Cargo binaries, registry cache |
| `/home/developer/.vscode-server` | Named volume `devbox-vscode` (if configured) | ✅ Yes | VS Code server and extensions |
| `/home/developer/.config/opencode` | Named volume `devbox-opencode-config` | ✅ Yes | `opencode.jsonc`, skills, plus `oh-my-opencode-slim.json` on the OMOS variant |
**opencode config** (`opencode.jsonc`) is auto-generated from `OPENCODE_PROVIDER` on each start. It sets provider and model only — no MCP servers. To persist config changes and use custom settings, use the named volume (default) or bind-mount from host (see Custom opencode config above).
## Source
MIT licensed. Source, issues, and `docker-compose.yml` templates: <https://gitea.jordbo.se/joakimp/opencode-devbox>
> This description is generated by `scripts/generate-dockerhub-md.py` from a hand-maintained template. Edit the template (not this file) and regenerate.
+84 -196
View File
@@ -1,15 +1,30 @@
#!/usr/bin/env python3
"""
Generate DOCKER_HUB.md from README.md.
Generate DOCKER_HUB.md.
Rationale
---------
README.md is the authoritative source. DOCKER_HUB.md is a subset
intended for users pulling the pre-built image from Docker Hub — so
build-from-source instructions, developer setup (git hooks, gitleaks),
and CI/contribution content are dropped.
DOCKER_HUB.md is the public-facing description shown on Docker Hub. It
has two hard constraints the README does not:
Docker Hub enforces a 25 kB limit on the full description field.
1. A 25 kB byte limit on the full_description field.
2. A different audience: Hub readers want a 30-second evaluation —
"what is this, how do I run it, does it have what I need" — and
reference material is better consulted in context on gitea.
For a long time this script tried to derive DOCKER_HUB.md from README.md
by section selection + targeted replacement. As the README grew that
approach pushed against the 25 kB ceiling on every change, costing a
trim-something-else exercise per edit (final state: 3 byte headroom).
The new approach is much simpler: a hand-written HUB_TEMPLATE below.
The template intentionally stays slim and links out to the gitea README
for everything that benefits from depth. README.md grows freely.
Trade-off: when image-variants table or quick-start flow changes,
update HUB_TEMPLATE here too. That coupling is now explicit and
local rather than spread across SECTION_RULES + REPLACEMENTS + TRIM
machinery.
Usage
-----
@@ -19,77 +34,42 @@ Regenerate in place:
Fail if DOCKER_HUB.md is out of sync with what this script would emit
(run this in CI):
python3 scripts/generate-dockerhub-md.py --check
Design
------
Sections are selected and in some cases rewritten via `SECTION_RULES`
below. This keeps the transformation explicit and easy to audit — if
a new section is added to README.md that should also appear on Docker
Hub, extend SECTION_RULES rather than inventing implicit heuristics.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
README = REPO_ROOT / "README.md"
DOCKER_HUB = REPO_ROOT / "DOCKER_HUB.md"
# Max size for Docker Hub full_description (bytes, UTF-8).
MAX_SIZE_BYTES = 25_000
# Per-section transformation.
#
# Each key is a top-level section title as it appears in README.md
# (without the leading "## ").
#
# The value is one of:
# "keep" — include verbatim.
# "drop" — exclude entirely.
# "replace" — substitute a custom body (see REPLACEMENTS).
# "trim" — keep but drop selected level-3 sub-sections listed
# in TRIM_SUBSECTIONS[title].
#
# Unknown sections default to "drop" with a warning — forcing an
# explicit decision whenever README gains a new section.
SECTION_RULES: dict[str, str] = {
"Why?": "drop", # build-motivation, not user-facing
"Quick Start": "replace", # swap docker compose clone flow for docker run
"Features": "keep",
"Usage": "keep",
"Configuration": "trim", # drop dev-build sub-sections
"oh-my-opencode-slim (Multi-Agent Orchestration)": "keep",
"pi (alternative/complementary harness)": "replace", # tailored Hub version, full detail in README
"AWS Bedrock Authentication": "keep",
"MemPalace — persistent AI memory": "keep",
"Gitea MCP server": "keep",
"Context7 MCP server": "drop",
"Shell defaults": "drop", # detail, full README covers it
"Secret Scanning": "drop", # dev-only — gitleaks is for committers
"Architecture": "keep",
"License": "replace", # point at source repo instead
}
# Where readers go for the full reference.
GITEA = "https://gitea.jordbo.se/joakimp/opencode-devbox"
# Level-3 sub-section titles (without the leading "### ") to drop from
# sections flagged as "trim". These are dev/build-oriented — Docker Hub
# users already have the image and don't need rebuild or multi-user
# compose instructions.
TRIM_SUBSECTIONS: dict[str, set[str]] = {
"Configuration": {
"Multi-user setup",
"Rebuilding the Image",
"Build Args",
},
}
# Replacement bodies. Keys match SECTION_RULES entries marked "replace".
# Each value is the full section including the "## Title" heading.
REPLACEMENTS: dict[str, str] = {
"Quick Start": """## Quick Start
HUB_TEMPLATE = f"""# opencode-devbox
Portable AI developer environment for [opencode](https://opencode.ai). Debian-based, with git, SSH, Node.js, AWS CLI v2, and common dev tools pre-installed.
Designed for teams who want a reproducible coding-agent setup that runs the same on every laptop and CI runner — without forcing each developer to install Bun, Node, AWS CLI, mempalace, or maintain shell config drift across machines.
## Image Variants
| Tag | Description |
|---|---|
| `latest` / `vX.Y.Z` | Base image — opencode, Node.js, AWS CLI, dev tools |
| `latest-omos` / `vX.Y.Z-omos` | Base + [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration and Bun |
| `latest-with-pi` / `vX.Y.Z-with-pi` | Base + [pi](https://github.com/mariozechner/pi-coding-agent) as alternative/complementary harness (shares the mempalace install with opencode) |
| `latest-omos-with-pi` / `vX.Y.Z-omos-with-pi` | OMOS + pi together |
All variants support `linux/amd64` and `linux/arm64`.
## Quick Start
```bash
docker run -it --rm \\
@@ -102,9 +82,9 @@ docker run -it --rm \\
joakimp/opencode-devbox:latest
```
This drops you straight into opencode with your project mounted at `/workspace`.
Drops you straight into opencode with your project mounted at `/workspace`.
For an interactive shell first (useful for AWS SSO login):
For an interactive shell first (useful for AWS SSO login, multi-harness workflows, or just `bash`):
```bash
docker run -it --rm \\
@@ -115,156 +95,63 @@ docker run -it --rm \\
joakimp/opencode-devbox:latest bash
```
Then run `opencode` when ready.
Then run `opencode`, `pi` (on `*-with-pi` variants), or `aws sso login` from the shell.
For docker-compose users, see the source repo for `docker-compose.yml` and `.env.example` templates.
""",
"License": """## Source
For docker-compose users, the source repo provides `docker-compose.yml`, `.env.example`, and a one-liner `docker compose up -d` workflow with named volumes pre-wired.
MIT licensed. Source, issues, and `docker-compose.yml` templates: <https://gitea.jordbo.se/joakimp/opencode-devbox>
""",
"pi (alternative/complementary harness)": """## pi (alternative/complementary harness)
## What's Inside
[pi](https://github.com/mariozechner/pi-coding-agent) is a lightweight TUI coding-agent that can run alongside opencode in the same container. Both harnesses share the same mempalace install and palace data — wing/diary entries created by one are visible to the other. Available on the `*-with-pi` and `*-omos-with-pi` image tags.
- **[opencode](https://opencode.ai)** — primary coding-agent harness. Multi-provider (Anthropic, OpenAI, Bedrock, Google, Groq, etc.).
- **[pi](https://github.com/mariozechner/pi-coding-agent)** *(in `*-with-pi` variants)* — lightweight TUI coding-agent that coexists with opencode and shares the same mempalace install. Includes the `mcp-loader` extension so any local-stdio or remote streamable-HTTP MCP server (searxng, gitea, context7, …) can be added by editing `~/.pi/agent/settings.json`.
- **[mempalace](https://github.com/MemPalace/mempalace)** — persistent AI memory layer (ChromaDB + SQLite). Wing/diary/knowledge-graph entries are mutually visible to opencode and pi.
- **[oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim)** *(in `*-omos` variants)* — multi-agent orchestration on top of opencode (council, fallback chains, named agents).
- **AWS CLI v2** with SSO support, **Node.js LTS**, **Bun** (OMOS variants), **uv** (Python), **gosu** for clean UID/GID adjustment to match your host workspace.
- **MCP wrappers** for mempalace pre-installed and pre-wired to both harnesses.
### Run
## Authentication
The default container CMD is `bash -l`, so `compose run --rm devbox` drops to a login shell. From there pick a harness:
The container reads provider credentials from environment variables and host-mounted config:
```bash
docker compose run --rm devbox # bash — then run `pi`, `opencode`, or `aws sso login`
docker compose run --rm devbox pi # launch pi directly
docker compose exec -u developer devbox pi # attach into a `compose up -d` container
```
- **Anthropic / OpenAI / Groq / others:** set `OPENCODE_PROVIDER` and the corresponding `*_API_KEY` via `-e` or `.env`.
- **AWS Bedrock (SSO):** mount `~/.aws` from the host, `OPENCODE_PROVIDER=amazon-bedrock`, then `aws sso login` inside the container. Tokens persist across container restarts via the host bind-mount.
- **OAuth / device-code providers:** auth state lives in opencode's config, which is persisted via the `devbox-opencode-config` named volume.
### MemPalace integration
Full Bedrock walkthrough (IAM roles, permissions, multi-account setups): see the [AWS Bedrock Authentication](
{GITEA}#aws-bedrock-authentication
) section on gitea.
pi ships pre-wired with the mempalace bridge — the `mempalace.ts` extension is symlinked into `~/.pi/agent/extensions/` at first start, exposing palace MCP tools (search, diary, knowledge graph) to pi natively. The same `~/.mempalace/` palace is shared with opencode, so memories are mutually visible across harnesses. To persist palace data across container recreate, uncomment the `devbox-palace` volume in `docker-compose.yml` (see *MemPalace* section below).
## Persistence
### Persistence
| Path in container | Volume | Contains |
| Volume | Mount | Survives |
|---|---|---|
| `/home/developer/.pi` | `devbox-pi-config` (default) | `settings.json`, `agent/extensions/`, `auth.json`, `sessions/`, plus `git/` and `npm-global/` (`NPM_CONFIG_PREFIX` points here) so `pi install npm:` / `git:` and `npm i -g` survive container recreate and image rebuild. |
| `/home/developer/.mempalace` | `devbox-palace` (uncomment) | Shared palace — visible to pi and opencode |
| `devbox-opencode-config` | `~/.config/opencode` | container recreate, image rebuild |
| `devbox-pi-config` | `~/.pi` | container recreate, image rebuild — incl. user-installed pi packages via `pi install` (`NPM_CONFIG_PREFIX` points into the volume) |
| `devbox-palace` (uncomment) | `~/.mempalace` | container recreate, image rebuild — palace data is precious, treat as primary storage |
| `devbox-chroma-cache` | `~/.cache/chroma` | container recreate (model cache, disposable — re-downloads in seconds) |
Baked pi (`/usr/bin/pi`, `/opt/pi-*`) ships in the image; rebuild to upgrade. `npm i -g @mariozechner/pi-coding-agent` lands on the volume and wins via `PATH`.
Workspace bind-mount (`/workspace`) is your project directory on the host, so source code is never inside the container.
Full build args, extension list, and toolkit detail: <https://gitea.jordbo.se/joakimp/opencode-devbox#pi-alternativecomplementary-harness>
""",
}
Full persistence reference, including multi-user (`SIGNUM`) isolation and host bind-mount alternatives: see the [README on gitea]({GITEA}#persistence).
## Where to Go Next
# Prepended to the generated file.
HEADER = """# opencode-devbox — Docker Hub
- **Full README** with build args, every feature in detail, troubleshooting: <{GITEA}>
- **CHANGELOG** for version history: <{GITEA}/src/branch/main/CHANGELOG.md>
- **Issues / source / docker-compose templates:** <{GITEA}>
- **Agent-facing internals** (for future maintainers / coding agents working in the repo): <{GITEA}/src/branch/main/AGENTS.md>
Portable AI developer environment for [opencode](https://opencode.ai). Debian-based, with git, SSH, Node.js, AWS CLI v2, and common dev tools pre-installed.
## License
## Image Variants
MIT. See <{GITEA}/src/branch/main/LICENSE>.
Four image variants are published for each release:
| Tag | Description |
|---|---|
| `latest` / `vX.Y.Z` | Base image — opencode, Node.js, AWS CLI, dev tools |
| `latest-omos` / `vX.Y.Z-omos` | Base + [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration and Bun |
| `latest-with-pi` / `vX.Y.Z-with-pi` | Base + [pi](https://github.com/mariozechner/pi-coding-agent) as alternative/complementary harness (shares the mempalace install with opencode) |
| `latest-omos-with-pi` / `vX.Y.Z-omos-with-pi` | OMOS + pi together |
All variants support `linux/amd64` and `linux/arm64`.
> **NOTE:** This file is auto-generated from `README.md` by `scripts/generate-dockerhub-md.py`. Edit README.md and regenerate rather than editing this file directly.
---
> This description is generated by `scripts/generate-dockerhub-md.py` from a hand-maintained template. Edit the template (not this file) and regenerate.
"""
def split_sections(md: str) -> list[tuple[str, str]]:
"""Split markdown on level-2 headings, returning (title, body) pairs.
The body includes the heading line and everything up to (but not
including) the next level-2 heading or EOF. Content before the first
``## `` is returned with an empty title (the document preamble).
"""
pattern = re.compile(r"^## ", re.MULTILINE)
parts = pattern.split(md)
preamble, *rest = parts
sections: list[tuple[str, str]] = []
if preamble.strip():
sections.append(("", preamble))
for part in rest:
line, _, body = part.partition("\n")
sections.append((line.strip(), f"## {line}\n{body}"))
return sections
def trim_subsections(body: str, drop: set[str]) -> str:
"""Remove level-3 sub-sections whose title is in `drop`.
A sub-section starts at a line beginning with "### " and ends at
the next "### " or "## " (or EOF).
"""
if not drop:
return body
# Split on level-3 headings while preserving the level-2 header
# block. First piece is everything up to the first "### ".
parts = re.split(r"(^### .+\n)", body, flags=re.MULTILINE)
# parts alternates: [before_first_h3, "### Title\n", body, "### Title\n", body, ...]
kept: list[str] = [parts[0]] if parts else []
i = 1
while i < len(parts):
heading = parts[i]
content = parts[i + 1] if i + 1 < len(parts) else ""
title = heading[4:].strip()
if title not in drop:
kept.append(heading)
kept.append(content)
i += 2
return "".join(kept)
def generate() -> str:
"""Produce the DOCKER_HUB.md content string."""
readme = README.read_text(encoding="utf-8")
sections = split_sections(readme)
out: list[str] = [HEADER]
unknown: list[str] = []
for title, body in sections:
if title == "":
# README preamble is replaced by our HEADER; skip.
continue
rule = SECTION_RULES.get(title)
if rule is None:
unknown.append(title)
continue
if rule == "drop":
continue
if rule == "keep":
out.append(body.rstrip() + "\n\n")
elif rule == "trim":
trimmed = trim_subsections(body, TRIM_SUBSECTIONS.get(title, set()))
out.append(trimmed.rstrip() + "\n\n")
elif rule == "replace":
out.append(REPLACEMENTS[title].rstrip() + "\n\n")
else: # pragma: no cover — programmer error
raise AssertionError(f"unknown rule {rule!r} for section {title!r}")
if unknown:
print(
"ERROR: README.md contains sections not classified in "
"SECTION_RULES:\n - "
+ "\n - ".join(unknown)
+ "\n\nAdd each to SECTION_RULES in "
"scripts/generate-dockerhub-md.py (choose keep/drop/replace).",
file=sys.stderr,
)
raise SystemExit(2)
return "".join(out).rstrip() + "\n"
return HUB_TEMPLATE
def main() -> int:
@@ -290,11 +177,10 @@ def main() -> int:
existing = DOCKER_HUB.read_text(encoding="utf-8") if DOCKER_HUB.exists() else ""
if existing != content:
print(
"ERROR: DOCKER_HUB.md is out of sync with README.md.\n"
"ERROR: DOCKER_HUB.md is out of sync with the template.\n"
"Run: python3 scripts/generate-dockerhub-md.py",
file=sys.stderr,
)
# Show a small diff hint.
import difflib
diff = difflib.unified_diff(
@@ -307,14 +193,16 @@ def main() -> int:
sys.stderr.writelines(list(diff)[:80])
return 1
print(
f"OK: DOCKER_HUB.md is in sync with README.md "
f"({size} bytes, {MAX_SIZE_BYTES} limit).",
f"OK: DOCKER_HUB.md is in sync with HUB_TEMPLATE "
f"({size} bytes, {MAX_SIZE_BYTES} limit, "
f"{MAX_SIZE_BYTES - size} bytes headroom).",
)
return 0
DOCKER_HUB.write_text(content, encoding="utf-8")
print(
f"Wrote {DOCKER_HUB} ({size} bytes, {MAX_SIZE_BYTES} limit).",
f"Wrote {DOCKER_HUB} ({size} bytes, {MAX_SIZE_BYTES} limit, "
f"{MAX_SIZE_BYTES - size} bytes headroom).",
)
return 0