Compare commits
12 Commits
ee7cfae1ff
..
v2.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 046a5aeb3b | |||
| fac14e2c56 | |||
| cc890a90e7 | |||
| 1295c56930 | |||
| 0cbcea83dc | |||
| 4024c4d87b | |||
| 4499bcf6c3 | |||
| 281ccbaa70 | |||
| bf53a8eaa8 | |||
| a31ef52b00 | |||
| bab78044a1 | |||
| 61ec340e40 |
+7
-1
@@ -7,7 +7,7 @@
|
|||||||
OPENCODE_PROVIDER=anthropic
|
OPENCODE_PROVIDER=anthropic
|
||||||
|
|
||||||
# Model override (optional, defaults per provider)
|
# Model override (optional, defaults per provider)
|
||||||
# OPENCODE_MODEL=anthropic/claude-sonnet-4-6
|
# OPENCODE_MODEL=anthropic/claude-sonnet-5
|
||||||
|
|
||||||
# ── API Keys (set the one matching your provider) ────────────────────
|
# ── API Keys (set the one matching your provider) ────────────────────
|
||||||
# ANTHROPIC_API_KEY=
|
# ANTHROPIC_API_KEY=
|
||||||
@@ -117,3 +117,9 @@ SSH_KEY_PATH=~/.ssh
|
|||||||
# # on image pull. Independent of ENABLE_OMOS.
|
# # on image pull. Independent of ENABLE_OMOS.
|
||||||
# # See docs/omos-skills.md
|
# # See docs/omos-skills.md
|
||||||
# OMOS_RESET=false # Force regenerate oh-my-opencode-slim config on next start (does not affect skills)
|
# OMOS_RESET=false # Force regenerate oh-my-opencode-slim config on next start (does not affect skills)
|
||||||
|
|
||||||
|
# ── Background subagents (opencode experimental flag) ────────────────
|
||||||
|
# Baked ON in the image (ENV in Dockerfile.base) because OMOS V2+ default
|
||||||
|
# orchestration depends on opencode's native background subagents. Uncomment
|
||||||
|
# to opt out (e.g. to force blocking/foreground orchestration).
|
||||||
|
# OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false
|
||||||
|
|||||||
@@ -66,3 +66,27 @@ jobs:
|
|||||||
# ("no project was found"). Globbing the workflow files is the
|
# ("no project was found"). Globbing the workflow files is the
|
||||||
# supported way to lint a non-GitHub layout.
|
# supported way to lint a non-GitHub layout.
|
||||||
run: actionlint -color .gitea/workflows/*.yml
|
run: actionlint -color .gitea/workflows/*.yml
|
||||||
|
|
||||||
|
hadolint:
|
||||||
|
# Lint the two Dockerfiles that ARE the project (the shell/actions linting
|
||||||
|
# above never looked at them). Config — ignored rules + failure threshold
|
||||||
|
# — lives in .hadolint.yaml, which hadolint reads automatically, so a local
|
||||||
|
# `hadolint Dockerfile.base` reproduces CI exactly.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install hadolint (pinned)
|
||||||
|
env:
|
||||||
|
HADOLINT_VERSION: 2.14.0
|
||||||
|
run: |
|
||||||
|
curl -fsSL \
|
||||||
|
"https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-Linux-x86_64" \
|
||||||
|
-o /usr/local/bin/hadolint
|
||||||
|
chmod +x /usr/local/bin/hadolint
|
||||||
|
hadolint --version
|
||||||
|
|
||||||
|
- name: Run hadolint
|
||||||
|
run: hadolint Dockerfile.base Dockerfile.variant
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# hadolint configuration for opencode-devbox.
|
||||||
|
#
|
||||||
|
# Both Dockerfiles are linted in CI (.gitea/workflows/lint.yml → `hadolint`
|
||||||
|
# job). hadolint reads this file automatically, so a local
|
||||||
|
# `hadolint Dockerfile.base` reproduces CI exactly.
|
||||||
|
#
|
||||||
|
# The ignores below are DELIBERATE project choices — they mirror the
|
||||||
|
# philosophy of the shellcheck excludes already applied to `run:` steps
|
||||||
|
# (SHELLCHECK_OPTS in lint.yml). Anything NOT listed here still fails the
|
||||||
|
# build at `warning` and above, so new Dockerfile smells are caught going
|
||||||
|
# forward.
|
||||||
|
ignored:
|
||||||
|
- DL3008 # "pin apt versions" — intentionally unpinned: the base tracks
|
||||||
|
# Debian stable and runs `apt-get upgrade`, so pinning point
|
||||||
|
# versions would rot and fight security updates.
|
||||||
|
- DL3016 # "pin npm versions" — opencode / oh-my-opencode-slim ARE pinned,
|
||||||
|
# but via build-args (CI-resolved from npm), not the npm CLI.
|
||||||
|
- DL4006 # "set -o pipefail before a pipe" — the piped RUNs are
|
||||||
|
# download|extract steps with their own retries / `set -e`.
|
||||||
|
# Switching the global SHELL to bash is a larger, base-affecting
|
||||||
|
# change — tracked in IDEAS.md.
|
||||||
|
- DL3003 # "use WORKDIR, not cd" — cosmetic in the few `cd` RUNs here.
|
||||||
|
- SC2086 # "double-quote to prevent word-splitting" — the same code is
|
||||||
|
# excluded for shell `run:` steps in lint.yml; splitting is
|
||||||
|
# intentional in these contexts.
|
||||||
|
|
||||||
|
failure-threshold: warning
|
||||||
@@ -51,8 +51,7 @@ repo, which decoupled from the pi tool version at its own `v1.0.0`.
|
|||||||
- **PATCH** — opencode/tool version bumps and small fixes that don't change the
|
- **PATCH** — opencode/tool version bumps and small fixes that don't change the
|
||||||
contract. When a release pairs a tool bump with a feature, the feature wins
|
contract. When a release pairs a tool bump with a feature, the feature wins
|
||||||
and it's a minor.
|
and it's a minor.
|
||||||
- **Pre-flight check** — whenever an opencode bump is part of the release,
|
- **Pre-flight check** — **before triggering any release CI build, check whether opencode is behind and offer to bump it.** Compare the pinned `OPENCODE_VERSION` in `Dockerfile.variant` against the latest npm release (`./check-versions.sh Dockerfile.variant` flags `→ X available` for opencode — note the base-only pins like `GOSU_VERSION` live in `Dockerfile.base`, so run it against that too for a full sweep). **If the latest stable `opencode-ai` is newer than the pin, ASK THE USER whether to bump before the build — never bump silently;** a version bump is a functional change they may want to hold or schedule. When a bump *is* part of the release, re-check the `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` removal-trigger against the new version's release notes (see the bg-subagents note under *Critical conventions*), then verify the pin is real before claiming it in the CHANGELOG:
|
||||||
verify it is real before claiming it in the CHANGELOG:
|
|
||||||
```bash
|
```bash
|
||||||
npm view opencode-ai version # must equal the X.Y.Z you pin in Dockerfile.variant
|
npm view opencode-ai version # must equal the X.Y.Z you pin in Dockerfile.variant
|
||||||
```
|
```
|
||||||
@@ -101,13 +100,14 @@ curl -s https://api.github.com/repos/anomalyco/opencode/releases/tags/v1.15.10 |
|
|||||||
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.
|
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.
|
||||||
|
|
||||||
**Between releases the same coupling applies.** Doc drift is not just a release-day concern — a workflow tweak, entrypoint change, or `generate-config.py` refactor can leave any of these four files lying. Before committing a non-release change, grep the docs for references to what you touched: `git diff --name-only HEAD | xargs -I{} grep -l 'thing-you-changed' README.md AGENTS.md DOCKER_HUB.md .gitea/README.md .env.example`. If a doc says "four variants" / "two phases" / "runs on amd64 only" and your change made that no longer true, fix it in the same commit.
|
**Between releases the same coupling applies.** Doc drift is not just a release-day concern — a workflow tweak, entrypoint change, or `generate-config.py` refactor can leave any of these four files lying. Before committing a non-release change, grep the docs for references to what you touched: `git diff --name-only HEAD | xargs -I{} grep -l 'thing-you-changed' README.md AGENTS.md DOCKER_HUB.md .gitea/README.md .env.example`. If a doc says "four variants" / "two phases" / "runs on amd64 only" and your change made that no longer true, fix it in the same commit.
|
||||||
- **GitHub/Gitea-sourced binaries float by default** — gosu, fzf, git-lfs, gitleaks, 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) — mind project-specific arch-name deviations (gitleaks uses `x64`, bat/eza/zoxide use `x86_64`/`aarch64`, gosu uses `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.
|
- **GitHub/Gitea-sourced binaries float by default** — gosu, fzf, git-lfs, gitleaks, 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) — mind project-specific arch-name deviations (gitleaks uses `x64`, bat/eza/zoxide use `x86_64`/`aarch64`, gosu uses `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 — **and add its `*_VERSION` ARG to the README *Build Args* table** (the floated-`latest` row alongside `GOSU_VERSION, FZF_VERSION, …`). That table is a *separate* exhaustive list from the env-var table and has silently drifted before: `MICRO_VERSION`, `TEALDEER_VERSION`, and `TYPST_VERSION` were missing from it for three releases (v2.4.0–v2.7.0), caught only by the 2026-07-13 doc-drift audit.
|
||||||
- **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`.
|
- **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`.
|
||||||
- **`OMOS_VERSION` MUST be passed by CI as a concrete version**, not left at the `latest` default. The npm install step in `Dockerfile.variant` (`oh-my-opencode-slim@${OMOS_VERSION}`) produces an identical layer-hash when the ARG value is byte-identical across builds; combined with the registry buildcache (`base-buildcache`) the layer gets reused even when `latest` would have resolved to a newer upstream. This is the same class of bug that bit pi-devbox v0.74.0 → v0.75.5 (silent same-bytes-across-releases regression discovered 2026-05-23, fixed in pi-devbox v0.75.5b). It is currently *masked* in opencode-devbox by `OPENCODE_VERSION` being a hard-coded ARG that bumps every release — that bump invalidates the parent-chain cache key for the downstream omos layer — but the masking would fail the moment a `vN.N.Nb` opencode-version-unchanged release ships that only bumps omos. Preventative fix: `.gitea/workflows/docker-publish-split.yml` has a `resolve-versions` job that runs `npm view oh-my-opencode-slim version`, exposing the concrete value as an output that the omos smoke + build jobs consume via build-args. Smoke tests assert via the `EXPECTED_OMOS_VERSION` env var — would catch the regression on the next release rather than several releases later. **If you change the variant build-args list, the resolve-versions job, or the smoke EXPECTED_*_VERSION wiring, audit all affected jobs in lockstep.**
|
- **`OMOS_VERSION` MUST be passed by CI as a concrete version**, not left at the `latest` default. The npm install step in `Dockerfile.variant` (`oh-my-opencode-slim@${OMOS_VERSION}`) produces an identical layer-hash when the ARG value is byte-identical across builds; combined with the registry buildcache (`base-buildcache`) the layer gets reused even when `latest` would have resolved to a newer upstream. This is the same class of bug that bit pi-devbox v0.74.0 → v0.75.5 (silent same-bytes-across-releases regression discovered 2026-05-23, fixed in pi-devbox v0.75.5b). It is currently *masked* in opencode-devbox by `OPENCODE_VERSION` being a hard-coded ARG that bumps every release — that bump invalidates the parent-chain cache key for the downstream omos layer — but the masking would fail the moment a `vN.N.Nb` opencode-version-unchanged release ships that only bumps omos. Preventative fix: `.gitea/workflows/docker-publish-split.yml` has a `resolve-versions` job that runs `npm view oh-my-opencode-slim version`, exposing the concrete value as an output that the omos smoke + build jobs consume via build-args. Smoke tests assert via the `EXPECTED_OMOS_VERSION` env var — would catch the regression on the next release rather than several releases later. **If you change the variant build-args list, the resolve-versions job, or the smoke EXPECTED_*_VERSION wiring, audit all affected jobs in lockstep.**
|
||||||
- **`resolve-versions` also pins `mempalace-toolkit` to a SHA** — `resolve-versions` resolves the `mempalace-toolkit` `main` HEAD to a commit SHA (`mempalace_toolkit_ref` output) via the gitea commits API (`/api/v1/repos/joakimp/mempalace-toolkit/commits?limit=1&sha=main`; gitea allows **unauthenticated** public-repo listing, so no secret is required). Unlike every other dependency, `mempalace-toolkit` is cloned in **`Dockerfile.base`**, not the variant — so the resolve→build-arg→variant plumbing bypasses it. To make a moved toolkit actually land, the resolved SHA is **folded into the `base-decide` hash** (so `base_tag` changes → base rebuilds) AND passed to `build-base` as `--build-arg MEMPALACE_TOOLKIT_REF`. Consequently **`base-decide` now depends on `resolve-versions`** (they no longer run in parallel), and the base clone uses a SHA-capable `git fetch <ref> + checkout FETCH_HEAD` (a `git clone --branch <40-char-SHA>` would fail). Trade-off: `base_tag` now reflects a live gitea lookup — on an API blip it falls back to `main`, triggering one *extra* base rebuild, never a *missed* one. If you touch `resolve-versions`, `base-decide`'s hash inputs, or the `build-base` build-args, audit `.gitea/README.md` Step 1 in lockstep.
|
- **`resolve-versions` also pins `mempalace-toolkit` to a SHA** — `resolve-versions` resolves the `mempalace-toolkit` `main` HEAD to a commit SHA (`mempalace_toolkit_ref` output) via the gitea commits API (`/api/v1/repos/joakimp/mempalace-toolkit/commits?limit=1&sha=main`; gitea allows **unauthenticated** public-repo listing, so no secret is required). Unlike every other dependency, `mempalace-toolkit` is cloned in **`Dockerfile.base`**, not the variant — so the resolve→build-arg→variant plumbing bypasses it. To make a moved toolkit actually land, the resolved SHA is **folded into the `base-decide` hash** (so `base_tag` changes → base rebuilds) AND passed to `build-base` as `--build-arg MEMPALACE_TOOLKIT_REF`. Consequently **`base-decide` now depends on `resolve-versions`** (they no longer run in parallel), and the base clone uses a SHA-capable `git fetch <ref> + checkout FETCH_HEAD` (a `git clone --branch <40-char-SHA>` would fail). Trade-off: `base_tag` now reflects a live gitea lookup — on an API blip it falls back to `main`, triggering one *extra* base rebuild, never a *missed* one. If you touch `resolve-versions`, `base-decide`'s hash inputs, or the `build-base` build-args, audit `.gitea/README.md` Step 1 in lockstep.
|
||||||
- **Registry buildkit cache-export is currently disabled** — do NOT re-add `cache-from`/`cache-to` to the `build-base` step in `.gitea/workflows/docker-publish-split.yml` without first verifying that buildkit's `mode=max` cache-export to `registry-1.docker.io` no longer returns HTTP 400 from the Hub CDN edge. The regression surfaced ~2026-05-23 and broke five consecutive opencode-devbox publish attempts (runs #332/333/334/336 + a rerun); root-caused on 2026-05-28 by a manual host-side publish that reproduced the same 400 only on `--cache-to` while image push worked fine. Failure shape is stable (`Offset:0` in the `_state` token, HTML response body = CDN-tier rejection, not registry backend), repo-specific (we're the only repo writing `:base-buildcache` mode=max), and explains why pinning `setup-buildx-action@v4.0.0` didn't help (action pin doesn't change the bundled buildkit version on the catthehacker runner image). Trade-off: dockerfile.base changes pay a full ~3 min rebuild instead of pulling cached layers; unchanged bases short-circuit at the Hub-probe step in `base-decide` and never re-build anyway. Variants don't use registry cache so they're unaffected. Re-enable condition: upstream moby/buildkit fix lands AND a low-risk test run succeeds without 400s. See CHANGELOG v1.15.12 `Unreleased` block for the full diagnostic chain. Manual escape-hatch publish procedure: `docs/manual-host-publish.md`.
|
- **Registry buildkit cache-export is currently disabled** — do NOT re-add `cache-from`/`cache-to` to the `build-base` step in `.gitea/workflows/docker-publish-split.yml` without first verifying that buildkit's `mode=max` cache-export to `registry-1.docker.io` no longer returns HTTP 400 from the Hub CDN edge. The regression surfaced ~2026-05-23 and broke five consecutive opencode-devbox publish attempts (runs #332/333/334/336 + a rerun); root-caused on 2026-05-28 by a manual host-side publish that reproduced the same 400 only on `--cache-to` while image push worked fine. Failure shape is stable (`Offset:0` in the `_state` token, HTML response body = CDN-tier rejection, not registry backend), repo-specific (we're the only repo writing `:base-buildcache` mode=max), and explains why pinning `setup-buildx-action@v4.0.0` didn't help (action pin doesn't change the bundled buildkit version on the catthehacker runner image). Trade-off: dockerfile.base changes pay a full ~3 min rebuild instead of pulling cached layers; unchanged bases short-circuit at the Hub-probe step in `base-decide` and never re-build anyway. Variants don't use registry cache so they're unaffected. Re-enable condition: upstream moby/buildkit fix lands AND a low-risk test run succeeds without 400s. See CHANGELOG v1.15.12 `Unreleased` block for the full diagnostic chain. Manual escape-hatch publish procedure: `docs/manual-host-publish.md`.
|
||||||
- **Push steps wrap `docker buildx build --push` in a 3-attempt retry loop** (15s, 30s backoff) for transient `registry-1.docker.io` blips — rate limits, brief 5xx, CDN flap. Implemented as inline `shell: bash` steps with `docker buildx build` raw rather than `docker/build-push-action@v7` so the loop is visible and tweakable. Affects the 1 base + 5 variant push steps in `.gitea/workflows/docker-publish-split.yml`; smoke-test builds (`load: true`, no push) are untouched. **This does NOT mask deterministic failures** — a true regression (like the cache-export 400 of 2026-05-23..28) fails all 3 attempts identically and the job still fails. Orthogonal to the cache-export disablement above: cache-export was about a deterministic protocol mismatch, retry is about absorbing genuine transients. Both are belt-and-braces with the `ci-release-watcher` skill's transient-rerun heuristic. If you change the matrix of push steps, keep the retry wrapper consistent across them — the pattern is duplicated rather than factored out because Gitea Actions doesn't support reusable composite shell steps cleanly.
|
- **Push steps wrap `docker buildx build --push` in a 3-attempt retry loop** (15s, 30s backoff) for transient `registry-1.docker.io` blips — rate limits, brief 5xx, CDN flap. Implemented as inline `shell: bash` steps with `docker buildx build` raw rather than `docker/build-push-action@v7` so the loop is visible and tweakable. Affects the 1 base + 5 variant push steps in `.gitea/workflows/docker-publish-split.yml`; smoke-test builds (`load: true`, no push) are untouched. **This does NOT mask deterministic failures** — a true regression (like the cache-export 400 of 2026-05-23..28) fails all 3 attempts identically and the job still fails. Orthogonal to the cache-export disablement above: cache-export was about a deterministic protocol mismatch, retry is about absorbing genuine transients. Both are belt-and-braces with the `ci-release-watcher` skill's transient-rerun heuristic. If you change the matrix of push steps, keep the retry wrapper consistent across them — the pattern is duplicated rather than factored out because Gitea Actions doesn't support reusable composite shell steps cleanly.
|
||||||
- **Shell scripts use `set -euo pipefail`** — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with `|| true`.
|
- **Shell scripts use `set -euo pipefail`** — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with `|| true`.
|
||||||
|
- **Background subagents flag baked ON — experimental, watch for promotion** — `Dockerfile.base` sets `ENV OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`. opencode gates native background subagents behind this flag (`packages/opencode/src/tool/task.ts` fails with `Background subagents require OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` when unset); `oh-my-opencode-slim` V2+ makes background orchestration its **default** workflow, so the omos variant is effectively degraded without it. It's a base ENV (applies to both variants; harmless on plain opencode — only *enables* a capability) and stays runtime-overridable (`-e …=false`). It's counted in the base hash, so editing that line advances `base-latest`. **REMOVAL TRIGGER:** when opencode promotes background subagents out of `EXPERIMENTAL_` (flag renamed or made default), drop the ENV. No upstream roadmap date as of opencode 1.17.20 / omos 2.2.0 (2026-07). Documented in lockstep in README env table, `.env.example`, and asserted by `scripts/smoke-test.sh` (`bg-subagents env baked`).
|
||||||
- **MemPalace `diary_write` anyOf workaround — upstream watch target** — `Dockerfile.base` carries a perl RUN block that strips a root-level `anyOf` from `mempalace_diary_write`'s advertised `inputSchema`. Mempalace 3.3.x/3.4.0 advertise `anyOf: [{required:[entry]},{required:[content]}]`, which Anthropic's tools API (and Codex) reject at session start (`input_schema does not support oneOf, allOf, or anyOf at the top level`), making the whole MCP server fail to load. The workaround is idempotent and self-deactivating: when upstream ships the real fix the regex stops matching and the build prints `WARN: ... upstream may have changed shape` — **that WARN is the signal to delete the RUN block.** Upstream status (last checked **2026-06-14**): issue **#1728 is still OPEN**; PR **#1735 is CLOSED UNMERGED (2026-06-11) — do NOT watch it, it is dead**; PR **#1717 is the current live fix candidate**; mempalace PyPI latest is **3.4.0 (== our pin)**, so **no release contains the fix yet** and the workaround must stay. **Removal trigger:** a mempalace release **> 3.4.0** that actually strips the root-level `anyOf` lands on PyPI — then bump `MEMPALACE_VERSION` (in lockstep with pi-devbox) and drop the RUN block. NOTE: `MEMPALACE_VERSION` (the pip pin) and `MEMPALACE_TOOLKIT_REF` (the git ref for the `mempalace-toolkit` clone) are unrelated despite the shared prefix; do not conflate them.
|
- **MemPalace `diary_write` anyOf workaround — upstream watch target** — `Dockerfile.base` carries a perl RUN block that strips a root-level `anyOf` from `mempalace_diary_write`'s advertised `inputSchema`. Mempalace 3.3.x/3.4.0 advertise `anyOf: [{required:[entry]},{required:[content]}]`, which Anthropic's tools API (and Codex) reject at session start (`input_schema does not support oneOf, allOf, or anyOf at the top level`), making the whole MCP server fail to load. The workaround is idempotent and self-deactivating: when upstream ships the real fix the regex stops matching and the build prints `WARN: ... upstream may have changed shape` — **that WARN is the signal to delete the RUN block.** Upstream status (last checked **2026-06-14**): issue **#1728 is still OPEN**; PR **#1735 is CLOSED UNMERGED (2026-06-11) — do NOT watch it, it is dead**; PR **#1717 is the current live fix candidate**; mempalace PyPI latest is **3.4.0 (== our pin)**, so **no release contains the fix yet** and the workaround must stay. **Removal trigger:** a mempalace release **> 3.4.0** that actually strips the root-level `anyOf` lands on PyPI — then bump `MEMPALACE_VERSION` (in lockstep with pi-devbox) and drop the RUN block. NOTE: `MEMPALACE_VERSION` (the pip pin) and `MEMPALACE_TOOLKIT_REF` (the git ref for the `mempalace-toolkit` clone) are unrelated despite the shared prefix; do not conflate them.
|
||||||
- **MemPalace install path** — installed via `uv tool install` into `/opt/uv-tools/mempalace/`. Both the `mempalace` CLI and the `mempalace-mcp` MCP server binary are shipped as entry points by the mempalace package itself and placed on PATH by uv as shims whose shebangs point at the venv's Python. No hand-rolled wrapper is needed. Do not use `pip install --break-system-packages` — that was the previous approach and has been removed. Do not use `["python3", "-m", "mempalace.mcp_server"]` in `opencode.jsonc` — system Python can't import from the uv venv.
|
- **MemPalace install path** — installed via `uv tool install` into `/opt/uv-tools/mempalace/`. Both the `mempalace` CLI and the `mempalace-mcp` MCP server binary are shipped as entry points by the mempalace package itself and placed on PATH by uv as shims whose shebangs point at the venv's Python. No hand-rolled wrapper is needed. Do not use `pip install --break-system-packages` — that was the previous approach and has been removed. Do not use `["python3", "-m", "mempalace.mcp_server"]` in `opencode.jsonc` — system Python can't import from the uv venv.
|
||||||
- **generate-config.py idempotency** — the script MUST never overwrite an existing `opencode.jsonc` or legacy `opencode.json`. Config persists in the `devbox-opencode-config` named volume; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this.
|
- **generate-config.py idempotency** — the script MUST never overwrite an existing `opencode.jsonc` or legacy `opencode.json`. Config persists in the `devbox-opencode-config` named volume; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this.
|
||||||
|
|||||||
+150
@@ -6,6 +6,156 @@ Tags follow **independent semver** (since `v2.0.0`) — they version *this image
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v2.8.0 — 2026-07-13
|
||||||
|
|
||||||
|
Minor release. Headline: **default models move to `claude-sonnet-5`** and opencode bumps `1.17.15 → 1.17.20`. Rounds out with a large user-docs backfill — five v2.4.0–v2.7.0 base features (typst PDF export, terminal terminfo, Neovim 24-bit colour, the first-shell host SSH reachability check, the baked global gitignore) that had shipped without README prose — plus the bg-subagents removal-trigger "last-checked" markers refreshed to current versions. The marker refresh edits `Dockerfile.base`, so `base-<hash>` advances and the **base image rebuilds** this release (not just the variants).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Default models bumped to `claude-sonnet-5`.** In
|
||||||
|
`rootfs/usr/local/lib/opencode-devbox/generate-config.py`,
|
||||||
|
`DEFAULT_MODELS["anthropic"]` → `anthropic/claude-sonnet-5` (was
|
||||||
|
`claude-sonnet-4-6`; also feeds `FALLBACK_MODEL`) and
|
||||||
|
`DEFAULT_MODELS["amazon-bedrock"]` →
|
||||||
|
`amazon-bedrock/global.anthropic.claude-sonnet-5` (was
|
||||||
|
`...claude-sonnet-4-5-20250929-v1:0`). Also updates the commented
|
||||||
|
`OPENCODE_MODEL` example in `.env.example`. Takes effect for new containers
|
||||||
|
when no explicit `OPENCODE_MODEL` override is set. The `openai` default
|
||||||
|
(`gpt-5.4`) is unchanged.
|
||||||
|
- **opencode bumped `1.17.15` → `1.17.20`** (`Dockerfile.variant`
|
||||||
|
`OPENCODE_VERSION`; latest stable on npm, verified via `npm view
|
||||||
|
opencode-ai version`). 1.17.16–1.17.20 are TUI/cosmetic and internal fixes
|
||||||
|
(command-palette flash, sub-agent task-row styling, an OpenAI Responses
|
||||||
|
workaround removal) — no config-affecting or breaking changes. The
|
||||||
|
`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` removal-trigger was re-checked
|
||||||
|
against these release notes and has **not** fired; the flag stays baked ON.
|
||||||
|
- **Refreshed the bg-subagents removal-trigger "last-checked" markers** (`Dockerfile.base`, `AGENTS.md`) from `opencode 1.17.15 / omos 2.1.0` to `opencode 1.17.20 / omos 2.2.0`. Comment-only and the trigger has still **not** fired — but the `Dockerfile.base` edit changes the base-content hash, so `base-<hash>` advances and **the base image rebuilds** on the next release (not just the variant).
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- **README backfill for v2.4.0–v2.7.0 features that shipped without user-facing docs:** PDF export via `pandoc --pdf-engine=typst` (new *Document conversion* section), terminal `TERM`/terminfo support (`ncurses-term` + `kitty-terminfo` + `xterm-ghostty`), Neovim 24-bit colour default (`termguicolors` via system `sysinit.vim`), the first-shell host SSH reachability check, and the baked global gitignore. Also adds the three floated build args missing from the Build Args table (`MICRO_VERSION`, `TEALDEER_VERSION`, `TYPST_VERSION`). Docs-only — no image change.
|
||||||
|
- **README + AGENTS.md polish:** refreshed stale `OPENCODE_VERSION=` build-arg examples (`1.5.0`, `1.17.8` → the current `1.17.20`), linked the new `THIRD_PARTY.md` from the *License* section, and added an AGENTS.md reminder to add any new floated `*_VERSION` ARG to the README *Build Args* table (the coupling gap that hid `MICRO`/`TEALDEER`/`TYPST` for three releases).
|
||||||
|
|
||||||
|
## v2.7.0 — 2026-07-13
|
||||||
|
|
||||||
|
Minor release. Headline: **further parity with the sibling `pi-devbox` image** —
|
||||||
|
ports the base additions from pi-devbox v1.4.0 + v1.5.0 that opencode-devbox
|
||||||
|
lacked. **PDF export now works out of the box** (`typst` as the pandoc PDF
|
||||||
|
engine — pandoc shipped since v2.6.0 as a front-end only), modern terminal
|
||||||
|
emulators resolve their `TERM` over SSH (ncurses-term + kitty-terminfo + a
|
||||||
|
compiled `xterm-ghostty` alias), Neovim renders in readable 24-bit colour by
|
||||||
|
default, and a first-shell **host SSH reachability check** warns when the Mac
|
||||||
|
host isn't reachable. Also adds repository hygiene (LICENSE, THIRD_PARTY.md,
|
||||||
|
hadolint CI, IDEAS backlog) and a `.claude` gitignore-seed pattern. All the
|
||||||
|
tooling/rootfs changes are base-affecting, so `base-<hash>` advances and the
|
||||||
|
base rebuilds.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`typst` — PDF engine for pandoc (Markdown→PDF).** `pandoc` shipped in the
|
||||||
|
base since v2.6.0 but as a front-end only — with no PDF back-end,
|
||||||
|
`pandoc -o out.pdf` failed with "xelatex not found". The base now installs
|
||||||
|
`typst`, a single ~30 MB static Rust binary (no LaTeX), used via
|
||||||
|
`pandoc --pdf-engine=typst`; a fuller TeX Live remains the higher-fidelity
|
||||||
|
fallback (install on demand). Also patches pandoc's bundled typst template
|
||||||
|
(`/usr/share/pandoc/data/templates/template.typst`), whose empty `font: ()`
|
||||||
|
default otherwise made a naked `--pdf-engine=typst` fail with "font fallback
|
||||||
|
list must not be empty" — defaulted to `Libertinus Serif` so PDF export works
|
||||||
|
without `-V mainfont`. Adds `xz-utils` to the apt layer (typst ships a
|
||||||
|
`.tar.xz`). Tracks `latest`; pin with `--build-arg TYPST_VERSION=vX.Y.Z`.
|
||||||
|
Ported from pi-devbox v1.4.0 (+ its v1.5.0 font-default fix). Base-affecting.
|
||||||
|
|
||||||
|
- **Terminal support beyond the default: `ncurses-term` + `kitty-terminfo` + a
|
||||||
|
compiled `xterm-ghostty` alias.** The base previously shipped only
|
||||||
|
`ncurses-base`, so SSHing in from a modern emulator degraded to a dumb
|
||||||
|
fallback. Now installs `ncurses-term` (terminfo for WezTerm, Alacritty, foot,
|
||||||
|
st, the base `ghostty` entry, and many more) and `kitty-terminfo`
|
||||||
|
(`xterm-kitty`), and compiles an `xterm-ghostty` alias with `tic -x`
|
||||||
|
(`use=ghostty`) — Ghostty connects as `TERM=xterm-ghostty` and no distro
|
||||||
|
packages that name. iTerm2's `xterm-256color` is already in ncurses-base. New
|
||||||
|
`rootfs/usr/local/share/terminfo-src/ghostty.terminfo`; the build asserts the
|
||||||
|
entry landed via `infocmp`. Ported from pi-devbox v1.5.0. Base-affecting.
|
||||||
|
|
||||||
|
- **Readable Neovim colours out of the box.** New system-wide Neovim config
|
||||||
|
(`rootfs/etc/xdg/nvim/sysinit.vim`) enables `termguicolors`. Vanilla Neovim
|
||||||
|
otherwise fell back to a muddy, low-contrast 256-colour palette over ssh.
|
||||||
|
`sysinit.vim` is Neovim's system vimrc — it loads for every user before any
|
||||||
|
personal `~/.config/nvim` and can still be overridden per-user
|
||||||
|
(`:set notermguicolors`). Pairs with `kitty-terminfo` above. Ported from
|
||||||
|
pi-devbox v1.5.0. Base-affecting.
|
||||||
|
|
||||||
|
- **Host SSH reachability check at shell startup.** `~/.bash_aliases` (baked
|
||||||
|
into the base) now runs a one-time SSH probe on the first bash session of each
|
||||||
|
container. If the Mac host is not reachable (Remote Login disabled or the
|
||||||
|
`devbox_jump` key not yet authorized) it prints a clear warning with the exact
|
||||||
|
two steps to fix it, including the container's public key inline. Subsequent
|
||||||
|
shells skip the check (a `/tmp` flag cleared on recreate); silent when SSH
|
||||||
|
works. Complements the existing `setup-lan-access.sh` key-generation message.
|
||||||
|
Ported from pi-devbox v1.4.0. Base-affecting.
|
||||||
|
|
||||||
|
- **Seeded global gitignore now ignores `**/.claude/settings.local.json`.**
|
||||||
|
Claude Code's per-machine local settings file holds machine-specific
|
||||||
|
permissions and can carry credentials, so it should never be committed. The
|
||||||
|
seed (`rootfs/home/developer/.gitignore_global`, baked to `/etc/skel-devbox/`)
|
||||||
|
gains the pattern. Existing containers are unaffected (the seed is copied only
|
||||||
|
when `~/.gitignore_global` is absent). Base-affecting (rootfs COPY).
|
||||||
|
|
||||||
|
- **Repository hygiene: `LICENSE`, `THIRD_PARTY.md`, `hadolint` CI, `IDEAS.md`.**
|
||||||
|
The repo declared MIT only in prose; it now ships an actual `LICENSE` file
|
||||||
|
(MIT, © Joakim Persson) plus `THIRD_PARTY.md` recording that the published
|
||||||
|
images bundle third-party software under its own terms (opencode, OMOS,
|
||||||
|
mempalace — MIT; gosu/uv/typst Apache-2.0; Debian packages under their own
|
||||||
|
licenses). A new `hadolint` job (pinned v2.14.0) in the lint workflow lints
|
||||||
|
`Dockerfile.base` and `Dockerfile.variant`; `.hadolint.yaml` grandfathers the
|
||||||
|
deliberate choices (unpinned apt/npm, `cd`-in-`RUN`, `SC2086` — mirroring the
|
||||||
|
existing shellcheck excludes) and fails on anything new at `warning`+.
|
||||||
|
`IDEAS.md` parks vetted-but-unscheduled follow-ups. Repo/CI only — not baked
|
||||||
|
into the image.
|
||||||
|
|
||||||
|
## v2.6.0 — 2026-07-08
|
||||||
|
|
||||||
|
Minor release. Headline: **CLI-toolset parity with the sibling `pi-devbox`
|
||||||
|
image** (adds `yq`, `pandoc`, `graphviz`, `tealdeer`/`tldr`, and the `dot-watch`
|
||||||
|
wrapper) plus **background subagents enabled by default**
|
||||||
|
(`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`), and an opencode bump
|
||||||
|
`1.17.13 → 1.17.15`. Touches `Dockerfile.base` (apt list, new binary, new ENV)
|
||||||
|
so the base image rebuilds (`base-<hash>` advances); the opencode bump rebuilds
|
||||||
|
the variant layers.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **CLI tooling parity with pi-devbox.** Added `yq` (YAML query/transform,
|
||||||
|
pairs with `jq`), `pandoc` (Markdown↔HTML/etc. document converter), and
|
||||||
|
`graphviz` (`dot` diagram rendering) via apt; `tealdeer` (the `tldr` command —
|
||||||
|
quick command examples) as a ~5 MB static musl binary via the floated
|
||||||
|
`TEALDEER_VERSION` arg; and the `dot-watch` wrapper (`rootfs/usr/local/bin/dot-watch`,
|
||||||
|
auto-renders a `.dot` file to PNG on save, graphviz-only). Deliberately did
|
||||||
|
**not** port `socat`, `imagemagick`, or the pi-studio-specific `studio-expose`
|
||||||
|
bridge. smoke-test asserts presence of all five.
|
||||||
|
- **Background subagents enabled by default (`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`).**
|
||||||
|
Baked as a base-level `ENV` in `Dockerfile.base`. opencode gates native
|
||||||
|
background subagents behind this experimental flag; OMOS V2+ makes background
|
||||||
|
orchestration its default workflow, so the omos variant was effectively
|
||||||
|
degraded without it. Overridable at runtime (`-e OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false`).
|
||||||
|
Documented in README env table and `.env.example`; smoke-test asserts the var
|
||||||
|
is present in the image environment. Marked experimental upstream — carries a
|
||||||
|
removal-trigger note in `Dockerfile.base` + AGENTS.md for when opencode
|
||||||
|
promotes it out of experimental (no upstream roadmap date as of opencode
|
||||||
|
1.17.15 / omos 2.1.0).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **opencode `1.17.13` → `1.17.15`.** v1.17.14 adds a code-mode MCP adapter for
|
||||||
|
confined orchestration scripts, hides the `execute` tool unless code mode is
|
||||||
|
enabled, and **fixes paginated MCP tool catalogs losing tool metadata +
|
||||||
|
output-schema validation** (relevant — this image wires the mempalace,
|
||||||
|
gitea-mcp, and context7 MCP servers). v1.17.15 is bugfixes + desktop/TUI UX
|
||||||
|
(Z.ai context-overflow error classification, macOS Sequoia titlebar, model
|
||||||
|
picker). No breaking changes. Release notes:
|
||||||
|
<https://github.com/anomalyco/opencode/releases>.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.5.0 — 2026-07-02
|
## v2.5.0 — 2026-07-02
|
||||||
|
|
||||||
Minor release. Headline: **optional shared/external MemPalace** —
|
Minor release. Headline: **optional shared/external MemPalace** —
|
||||||
|
|||||||
+87
-1
@@ -15,7 +15,7 @@
|
|||||||
# content-addressed over this file, so any byte change invalidates the
|
# content-addressed over this file, so any byte change invalidates the
|
||||||
# cache. Recommended cadence: once per release for security updates.
|
# cache. Recommended cadence: once per release for security updates.
|
||||||
#
|
#
|
||||||
# BASE_REBUILD_DATE: 2026-05-14 (v1.14.50b — fresh apt + first promote-base-latest)
|
# BASE_REBUILD_DATE: 2026-07-13 (v2.7.0 — typst PDF engine + terminal terminfo (ncurses-term/kitty/xterm-ghostty) + nvim true-colour; pi-devbox parity)
|
||||||
#
|
#
|
||||||
# See the project README's "Build pipeline" section for the rationale.
|
# See the project README's "Build pipeline" section for the rationale.
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@ RUN apt-get update && \
|
|||||||
openssh-client \
|
openssh-client \
|
||||||
gnupg \
|
gnupg \
|
||||||
jq \
|
jq \
|
||||||
|
yq \
|
||||||
ripgrep \
|
ripgrep \
|
||||||
fd-find \
|
fd-find \
|
||||||
tree \
|
tree \
|
||||||
@@ -72,6 +73,11 @@ RUN apt-get update && \
|
|||||||
python3-pip \
|
python3-pip \
|
||||||
python3-venv \
|
python3-venv \
|
||||||
nano \
|
nano \
|
||||||
|
pandoc \
|
||||||
|
xz-utils \
|
||||||
|
graphviz \
|
||||||
|
kitty-terminfo \
|
||||||
|
ncurses-term \
|
||||||
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
|
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -295,6 +301,56 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64"
|
|||||||
rm -rf /tmp/uv-* && \
|
rm -rf /tmp/uv-* && \
|
||||||
uv --version
|
uv --version
|
||||||
|
|
||||||
|
# tealdeer — Rust port of tldr (community-maintained command examples)
|
||||||
|
# Provides the `tldr` command; ~5 MB static binary, ~135 MB smaller than
|
||||||
|
# the Node tldr global. Same UX as the Node version.
|
||||||
|
ARG TEALDEER_VERSION=latest
|
||||||
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||||
|
V="${TEALDEER_VERSION}" && \
|
||||||
|
if [ "$V" = "latest" ]; then \
|
||||||
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tealdeer-rs/tealdeer/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||||
|
fi && \
|
||||||
|
V="${V#v}" && \
|
||||||
|
[ -n "$V" ] && \
|
||||||
|
echo "Installing tealdeer ${V}" && \
|
||||||
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tealdeer-rs/tealdeer/releases/download/v${V}/tealdeer-linux-${ARCH}-musl" -o /usr/local/bin/tldr && \
|
||||||
|
chmod +x /usr/local/bin/tldr && \
|
||||||
|
tldr --version
|
||||||
|
|
||||||
|
# ── typst — lightweight PDF engine for pandoc (Markdown→PDF) ─────────
|
||||||
|
# pandoc (apt-installed above) is only a front-end; rendering PDF needs a
|
||||||
|
# back-end engine. Rather than a ~600 MB TeX Live install, we ship typst:
|
||||||
|
# a single ~30 MB static Rust binary with no LaTeX dependency, used via
|
||||||
|
# `pandoc --pdf-engine=typst`. A fuller TeX Live remains the higher-fidelity
|
||||||
|
# fallback for anyone who needs LaTeX-exact output (install on demand).
|
||||||
|
# Ported from pi-devbox (its v1.4.0 + v1.5.0 font-default fix).
|
||||||
|
#
|
||||||
|
# Follows the `latest` GitHub-release convention (like tealdeer/uv/bat).
|
||||||
|
# typst ships a `.tar.xz` asset (hence xz-utils in the apt layer above)
|
||||||
|
# that extracts to typst-<arch>-unknown-linux-musl/typst. Pin a specific
|
||||||
|
# tag with --build-arg TYPST_VERSION=vX.Y.Z.
|
||||||
|
#
|
||||||
|
# We also patch pandoc's bundled typst template
|
||||||
|
# (/usr/share/pandoc/data/templates/template.typst): its conf() defaults the
|
||||||
|
# document font to an empty tuple (`font: ()`), so a naked
|
||||||
|
# `pandoc --pdf-engine=typst` fails with "font fallback list must not be empty"
|
||||||
|
# unless the caller passes `-V mainfont=...`. We default it to Libertinus Serif
|
||||||
|
# (typst's own bundled default font) so PDF export works out-of-the-box.
|
||||||
|
ARG TYPST_VERSION=latest
|
||||||
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||||
|
V="${TYPST_VERSION}" && \
|
||||||
|
if [ "$V" = "latest" ]; then \
|
||||||
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/typst/typst/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||||
|
fi && \
|
||||||
|
V="${V#v}" && [ -n "$V" ] && \
|
||||||
|
echo "Installing typst ${V}" && \
|
||||||
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/typst/typst/releases/download/v${V}/typst-${ARCH}-unknown-linux-musl.tar.xz" | tar -xJ -C /tmp && \
|
||||||
|
install /tmp/typst-${ARCH}-unknown-linux-musl/typst /usr/local/bin/typst && \
|
||||||
|
rm -rf /tmp/typst-${ARCH}-unknown-linux-musl && \
|
||||||
|
typst --version && \
|
||||||
|
sed -i 's/^ font: (),$/ font: ("Libertinus Serif",),/' /usr/share/pandoc/data/templates/template.typst && \
|
||||||
|
grep -q 'font: ("Libertinus Serif",),' /usr/share/pandoc/data/templates/template.typst
|
||||||
|
|
||||||
# ── MemPalace — local-first AI memory system ─────────────────────────
|
# ── MemPalace — local-first AI memory system ─────────────────────────
|
||||||
# Provides semantic search over conversation history via 29 MCP tools.
|
# Provides semantic search over conversation history via 29 MCP tools.
|
||||||
# Always installed in the base (variant-independent). Set
|
# Always installed in the base (variant-independent). Set
|
||||||
@@ -384,6 +440,17 @@ ENV LANGUAGE=en_US:en
|
|||||||
ENV LC_ALL=en_US.UTF-8
|
ENV LC_ALL=en_US.UTF-8
|
||||||
ENV EDITOR=nvim
|
ENV EDITOR=nvim
|
||||||
ENV PATH="/home/developer/.local/bin:/home/developer/.cargo/bin:${PATH}"
|
ENV PATH="/home/developer/.local/bin:/home/developer/.cargo/bin:${PATH}"
|
||||||
|
# Enable opencode's native background subagents. opencode gates this behind an
|
||||||
|
# experimental flag (packages/opencode/src/tool/task.ts errors with
|
||||||
|
# "Background subagents require OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true"
|
||||||
|
# when unset). oh-my-opencode-slim V2+ makes background orchestration its DEFAULT
|
||||||
|
# workflow, so the omos variant is effectively broken without this. Baked here as
|
||||||
|
# a base ENV (applies to both variants; harmless for plain opencode — it only
|
||||||
|
# *enables* a capability). Overridable at runtime: -e OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false.
|
||||||
|
# REMOVAL TRIGGER: when opencode promotes background subagents out of experimental
|
||||||
|
# (flag becomes default / renamed), drop this ENV. No upstream roadmap date as of
|
||||||
|
# opencode 1.17.20 / omos 2.2.0 (2026-07).
|
||||||
|
ENV OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
|
||||||
|
|
||||||
# ── Node.js (required for opencode/pi/omos at variant build + MCP servers) ──
|
# ── Node.js (required for opencode/pi/omos at variant build + MCP servers) ──
|
||||||
ARG NODE_VERSION=22
|
ARG NODE_VERSION=22
|
||||||
@@ -464,8 +531,26 @@ COPY rootfs/home/developer/.bash_aliases /etc/skel-devbox/.bash_aliases
|
|||||||
COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
|
COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
|
||||||
COPY rootfs/home/developer/.gitignore_global /etc/skel-devbox/.gitignore_global
|
COPY rootfs/home/developer/.gitignore_global /etc/skel-devbox/.gitignore_global
|
||||||
|
|
||||||
|
# ── Editor defaults: system-wide Neovim true-colour ──────────────────
|
||||||
|
# /etc/xdg/nvim/sysinit.vim is Neovim's system vimrc: it loads for every user
|
||||||
|
# (before any personal ~/.config/nvim) and can still be overridden per-user.
|
||||||
|
# Enables termguicolors so the default theme renders in 24-bit colour instead
|
||||||
|
# of a muddy 256-colour fallback. Pairs with kitty-terminfo (installed above).
|
||||||
|
COPY rootfs/etc/xdg/nvim/sysinit.vim /etc/xdg/nvim/sysinit.vim
|
||||||
|
|
||||||
|
# ── Terminal support: xterm-ghostty terminfo alias ──────────────────
|
||||||
|
# ncurses-term (installed above) covers wezterm/alacritty/foot/st and the base
|
||||||
|
# `ghostty` entry, but Ghostty connects with TERM=xterm-ghostty, for which no
|
||||||
|
# distro packages an entry. Ship a thin alias (use=ghostty) and compile it into
|
||||||
|
# the system terminfo db with `tic -x`, so it inherits the maintained ghostty
|
||||||
|
# capability set. The `infocmp` check fails the build if the entry didn't land.
|
||||||
|
COPY rootfs/usr/local/share/terminfo-src/ghostty.terminfo /usr/local/share/terminfo-src/ghostty.terminfo
|
||||||
|
RUN tic -x -o /usr/share/terminfo /usr/local/share/terminfo-src/ghostty.terminfo && \
|
||||||
|
infocmp -x xterm-ghostty >/dev/null
|
||||||
|
|
||||||
# ── Entrypoint ────────────────────────────────────────────────────────
|
# ── Entrypoint ────────────────────────────────────────────────────────
|
||||||
COPY rootfs/usr/local/lib/opencode-devbox/ /usr/local/lib/opencode-devbox/
|
COPY rootfs/usr/local/lib/opencode-devbox/ /usr/local/lib/opencode-devbox/
|
||||||
|
COPY rootfs/usr/local/bin/dot-watch /usr/local/bin/dot-watch
|
||||||
# Image-baked skills + harness instruction. Under /usr/local so a named volume
|
# Image-baked skills + harness instruction. Under /usr/local so a named volume
|
||||||
# over a home dir (e.g. devbox-opencode-config on ~/.config/opencode) can't
|
# over a home dir (e.g. devbox-opencode-config on ~/.config/opencode) can't
|
||||||
# shadow them; entrypoint-user.sh links them into ~/.agents/skills/ and
|
# shadow them; entrypoint-user.sh links them into ~/.agents/skills/ and
|
||||||
@@ -475,6 +560,7 @@ COPY rootfs/usr/local/share/opencode-devbox/ /usr/local/share/opencode-devbox/
|
|||||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
|
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
|
||||||
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
|
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
|
||||||
|
/usr/local/bin/dot-watch \
|
||||||
/usr/local/lib/opencode-devbox/*.py
|
/usr/local/lib/opencode-devbox/*.py
|
||||||
|
|
||||||
# Start as root — entrypoint adjusts UID/GID then drops to developer
|
# Start as root — entrypoint adjusts UID/GID then drops to developer
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ ARG USER_NAME=developer
|
|||||||
# edit, so the cache-hit class of bug that bit pi-devbox v0.74.0..
|
# edit, so the cache-hit class of bug that bit pi-devbox v0.74.0..
|
||||||
# v0.75.5 cannot apply here.
|
# v0.75.5 cannot apply here.
|
||||||
ARG INSTALL_OPENCODE=true
|
ARG INSTALL_OPENCODE=true
|
||||||
ARG OPENCODE_VERSION=1.17.13
|
ARG OPENCODE_VERSION=1.17.20
|
||||||
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
|
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
|
||||||
NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION} && \
|
NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION} && \
|
||||||
opencode --version ; \
|
opencode --version ; \
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Ideas & backlog
|
||||||
|
|
||||||
|
A living list of potential improvements for opencode-devbox that are **not yet
|
||||||
|
scheduled**. This is intentionally lightweight — a place to park ideas so they
|
||||||
|
aren't lost between sessions. When an item ships, describe it in
|
||||||
|
[`CHANGELOG.md`](CHANGELOG.md) and remove it from here.
|
||||||
|
|
||||||
|
Rough effort tags: 🟢 small · 🟡 medium · 🔴 large. Status: `idea` (unvetted) ·
|
||||||
|
`planned` (agreed, not started).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supply-chain hardening
|
||||||
|
|
||||||
|
- 🟡 `planned` — **Pin CI actions to commit SHAs.** The workflows use floating
|
||||||
|
major tags (`actions/checkout@v4`, `docker/build-push-action@v7`,
|
||||||
|
`docker/setup-buildx-action@v4`, `docker/login-action@v3`,
|
||||||
|
`docker/setup-qemu-action@v3`). This is inconsistent with the project's own
|
||||||
|
philosophy of SHA-pinning *content* refs to defeat floating refs. Pin each
|
||||||
|
action to a SHA with a trailing `# vX.Y.Z` comment. Pairs naturally with the
|
||||||
|
renovate item below to keep the pins fresh.
|
||||||
|
|
||||||
|
- 🟡 `planned` — **Vulnerability scanning in CI.** No CVE scan runs on the
|
||||||
|
published images today. Add a `trivy image` (or grype) job to
|
||||||
|
`docker-publish-split.yml` after `smoke`. Start non-blocking (report only),
|
||||||
|
then tighten to fail on `HIGH`/`CRITICAL` with an available fix.
|
||||||
|
|
||||||
|
- 🟢🟡 `planned` — **Standardize build provenance → buildx SBOM + attestations.**
|
||||||
|
The image already carries hand-rolled provenance (OCI labels +
|
||||||
|
`build-manifest`). `docker/build-push-action` can emit a standard SBOM and
|
||||||
|
SLSA provenance attestation nearly for free (`provenance: mode=max`,
|
||||||
|
`sbom: true`). Makes provenance machine-consumable and pairs well with the
|
||||||
|
trivy item (scan the SBOM).
|
||||||
|
|
||||||
|
## Dockerfile hardening
|
||||||
|
|
||||||
|
- 🟡 `idea` — **Address hadolint DL4006 properly.** Currently ignored in
|
||||||
|
`.hadolint.yaml`. The clean fix is `SHELL ["/bin/bash", "-o", "pipefail",
|
||||||
|
"-c"]` so piped `RUN`s fail on the first non-zero stage. This changes the
|
||||||
|
default `RUN` shell from `sh` to `bash` for all subsequent layers, so it is
|
||||||
|
base-affecting and needs a careful pass over existing `RUN`s before removing
|
||||||
|
the ignore.
|
||||||
|
|
||||||
|
## Developer experience
|
||||||
|
|
||||||
|
- 🟢 `idea` — **`Makefile`/`justfile` for local iteration.** Reproducing a CI
|
||||||
|
build locally means hand-assembling many `--build-arg`s. Thin targets
|
||||||
|
(`make build-base`, `make build-variant`, `make smoke`, `make lint`) would
|
||||||
|
make local testing painless and document the canonical invocations.
|
||||||
|
|
||||||
|
- 🟡 `idea` — **Dependency-update automation (renovate).** With CI actions
|
||||||
|
SHA-pinned (above), a `renovate.json` keeps those pins — plus the pinned tool
|
||||||
|
versions (`ACTIONLINT_VERSION`, `HADOLINT_VERSION`, gosu, etc.) — current via
|
||||||
|
automated PRs. Requires a renovate runner against the Gitea instance.
|
||||||
|
|
||||||
|
## Housekeeping
|
||||||
|
|
||||||
|
- 🟢 `idea` — **Registry retention for `base-<hash>` tags.** The base-hash
|
||||||
|
caching scheme accumulates `base-<hash>` tags over time. Confirm whether the
|
||||||
|
registry prunes old ones, and add a retention/cleanup step if not.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Joakim Persson
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -148,6 +148,7 @@ docker compose exec -u developer devbox aws --version
|
|||||||
| `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` |
|
| `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` |
|
||||||
| `OMOS_SKILLS` | Symlink bundled OMOS skills from the image into `~/.agents/skills/` each start | `true` |
|
| `OMOS_SKILLS` | Symlink bundled OMOS skills from the image into `~/.agents/skills/` each start | `true` |
|
||||||
| `OMOS_RESET` | Force regenerate OMOS config on next start (does not affect skills) | `false` |
|
| `OMOS_RESET` | Force regenerate OMOS config on next start (does not affect skills) | `false` |
|
||||||
|
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | Enable opencode's native background subagents. Baked on in the image because OMOS V2+ default orchestration depends on it. Set `false` to opt out. opencode marks this **experimental** — see [AGENTS.md](AGENTS.md) removal trigger | `true` |
|
||||||
| `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect |
|
| `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect |
|
||||||
|
|
||||||
### Reaching your LAN from the container
|
### Reaching your LAN from the container
|
||||||
@@ -263,11 +264,23 @@ volumes:
|
|||||||
- ~/.config/nvim:/home/developer/.config/nvim:ro
|
- ~/.config/nvim:/home/developer/.config/nvim:ro
|
||||||
```
|
```
|
||||||
|
|
||||||
|
24-bit colour is on by default — a system-wide `sysinit.vim` (in `/etc/xdg/nvim/`) enables `termguicolors` so strings and comments render in full colour over SSH instead of a muddy 256-colour palette. It loads before any personal config and stays overridable per-user (`:set notermguicolors`, or your own `~/.config/nvim/init`).
|
||||||
|
|
||||||
> **Not a vi person?** The image also ships two non-modal editors alongside nvim:
|
> **Not a vi person?** The image also ships two non-modal editors alongside nvim:
|
||||||
> **nano** (classic, minimal) and **micro** (modern — desktop-style `Ctrl+S`/`Ctrl+Q`
|
> **nano** (classic, minimal) and **micro** (modern — desktop-style `Ctrl+S`/`Ctrl+Q`
|
||||||
> keys, mouse, syntax highlighting). `EDITOR` stays `nvim`; opt in per-shell with
|
> keys, mouse, syntax highlighting). `EDITOR` stays `nvim`; opt in per-shell with
|
||||||
> `export EDITOR=nano` (or `micro`), or for git with `git config --global core.editor micro`.
|
> `export EDITOR=nano` (or `micro`), or for git with `git config --global core.editor micro`.
|
||||||
|
|
||||||
|
### Document conversion (Markdown → PDF)
|
||||||
|
|
||||||
|
The image ships [`pandoc`](https://pandoc.org/) with [`typst`](https://typst.app/) as the PDF engine, so Markdown → PDF works out of the box with no LaTeX install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pandoc README.md -o readme.pdf --pdf-engine=typst
|
||||||
|
```
|
||||||
|
|
||||||
|
The bundled pandoc typst template defaults the font to `Libertinus Serif`, so a bare `--pdf-engine=typst` renders without needing `-V mainfont`. For higher-fidelity or complex layouts, install TeX Live on demand and use `--pdf-engine=xelatex` instead.
|
||||||
|
|
||||||
### Python development with uv
|
### 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:
|
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:
|
||||||
@@ -439,7 +452,7 @@ Enable optional language runtimes, pin a specific opencode version, or lock any
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose build --build-arg INSTALL_GO=true
|
docker compose build --build-arg INSTALL_GO=true
|
||||||
docker compose build --build-arg OPENCODE_VERSION=1.5.0
|
docker compose build --build-arg OPENCODE_VERSION=1.17.20
|
||||||
docker compose build --build-arg NVIM_VERSION=0.12.1 # pin to a specific version
|
docker compose build --build-arg NVIM_VERSION=0.12.1 # pin to a specific version
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -452,7 +465,7 @@ docker compose build --build-arg NVIM_VERSION=0.12.1 # pin to a specific versi
|
|||||||
| `INSTALL_OPENCODE` | `true` | Install opencode. Set `false` to build a base with no harness (still includes Bun if `INSTALL_OMOS=true`). |
|
| `INSTALL_OPENCODE` | `true` | Install opencode. Set `false` to build a base with no harness (still includes Bun if `INSTALL_OMOS=true`). |
|
||||||
| `OPENCODE_VERSION` | *(pinned per release)* | opencode npm version. Drives the image tag and is intentionally not floated. |
|
| `OPENCODE_VERSION` | *(pinned per release)* | opencode npm version. Drives the image tag and is intentionally not floated. |
|
||||||
| `NODE_VERSION` | `22` | Node.js major version. Pinned to protect against upstream breaking changes across majors. |
|
| `NODE_VERSION` | `22` | Node.js major version. Pinned to protect against upstream breaking changes across majors. |
|
||||||
| `GOSU_VERSION`, `FZF_VERSION`, `GIT_LFS_VERSION`, `NVIM_VERSION`, `BAT_VERSION`, `EZA_VERSION`, `ZOXIDE_VERSION`, `UV_VERSION`, `GITEA_MCP_VERSION`, `GO_VERSION`, `OMOS_VERSION` | `latest` | All GitHub/Gitea/go.dev-hosted binaries resolve to the newest upstream release at build time. Override with a specific version to pin. Resolved versions are logged in CI output. |
|
| `GOSU_VERSION`, `FZF_VERSION`, `GIT_LFS_VERSION`, `NVIM_VERSION`, `BAT_VERSION`, `EZA_VERSION`, `ZOXIDE_VERSION`, `UV_VERSION`, `GITEA_MCP_VERSION`, `GO_VERSION`, `OMOS_VERSION`, `MICRO_VERSION`, `TEALDEER_VERSION`, `TYPST_VERSION` | `latest` | All GitHub/Gitea/go.dev-hosted binaries resolve to the newest upstream release at build time. Override with a specific version to pin. Resolved versions are logged in CI output. |
|
||||||
|
|
||||||
> **Reproducibility note:** With `latest` defaults, two builds of the same `v{opencode}` tag may embed different tool versions if upstream releases have happened in between. This is intentional — it means every rebuild picks up upstream CVE fixes automatically. If you need a bit-for-bit reproducible build, pass explicit `*_VERSION` args. The CI smoke test logs the resolved versions for every release build.
|
> **Reproducibility note:** With `latest` defaults, two builds of the same `v{opencode}` tag may embed different tool versions if upstream releases have happened in between. This is intentional — it means every rebuild picks up upstream CVE fixes automatically. If you need a bit-for-bit reproducible build, pass explicit `*_VERSION` args. The CI smoke test logs the resolved versions for every release build.
|
||||||
|
|
||||||
@@ -670,7 +683,7 @@ docker build -f Dockerfile.base -t myorg/opencode-devbox:base-dev \
|
|||||||
# then the variant FROM that base
|
# then the variant FROM that base
|
||||||
docker build -f Dockerfile.variant -t myorg/opencode-devbox:dev \
|
docker build -f Dockerfile.variant -t myorg/opencode-devbox:dev \
|
||||||
--build-arg BASE_IMAGE=myorg/opencode-devbox:base-dev \
|
--build-arg BASE_IMAGE=myorg/opencode-devbox:base-dev \
|
||||||
--build-arg OPENCODE_VERSION=1.17.8 .
|
--build-arg OPENCODE_VERSION=1.17.20 .
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: mempalace-toolkit clones anonymously (no token needed). Only the `resolve-versions` CI job calls the gitea API (which needs a token for public repos). A plain `docker build` like the above skips that job entirely, so no credentials are required.
|
Note: mempalace-toolkit clones anonymously (no token needed). Only the `resolve-versions` CI job calls the gitea API (which needs a token for public repos). A plain `docker build` like the above skips that job entirely, so no credentials are required.
|
||||||
@@ -760,6 +773,7 @@ Defaults you get out of the box:
|
|||||||
- **Aliases** — `ls`/`ll`/`la` use `eza`, `cat` uses `bat`, `gs`/`gd`/`gl` for git, safe `rm`/`mv`/`cp`.
|
- **Aliases** — `ls`/`ll`/`la` use `eza`, `cat` uses `bat`, `gs`/`gd`/`gl` for git, safe `rm`/`mv`/`cp`.
|
||||||
- **Integrations** — `zoxide` (`z <fragment>` to jump), `fzf` Ctrl-R / Ctrl-T key bindings.
|
- **Integrations** — `zoxide` (`z <fragment>` to jump), `fzf` Ctrl-R / Ctrl-T key bindings.
|
||||||
- **Prompt marker** — `[devbox]` prefix so it's always obvious you're inside the container.
|
- **Prompt marker** — `[devbox]` prefix so it's always obvious you're inside the container.
|
||||||
|
- **Host SSH reachability check** — on the first shell of a freshly (re)created container, a one-time probe checks whether the Docker host is reachable over SSH. If it isn't (host Remote Login off, or the container's `devbox_jump` key not yet authorized), it prints a clear two-step fix with the container's public key inline; it stays silent once SSH works and is skipped on later shells.
|
||||||
|
|
||||||
### Overriding the defaults
|
### Overriding the defaults
|
||||||
|
|
||||||
@@ -793,6 +807,14 @@ cp /etc/skel-devbox/.bash_aliases ~/.bash_aliases
|
|||||||
rm ~/.bash_aliases
|
rm ~/.bash_aliases
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Terminal compatibility
|
||||||
|
|
||||||
|
The base ships `ncurses-term` and `kitty-terminfo` on top of the default `ncurses-base`, plus a compiled `xterm-ghostty` alias, so modern terminal emulators resolve their `TERM` correctly over SSH instead of degrading to a dumb fallback. Covered out of the box: WezTerm, Alacritty, foot, st, kitty (`xterm-kitty`), Ghostty (`xterm-ghostty`), and iTerm2 / xterm (`xterm-256color`).
|
||||||
|
|
||||||
|
## Global gitignore
|
||||||
|
|
||||||
|
The image bakes a `~/.gitignore_global` and wires it via `git config --global core.excludesFile`, so personal/tooling artifacts are ignored across every repo in the container without per-repo `.gitignore` entries. Seeded patterns include `*.bak`, `*.bak.*`, `*~`, `*.orig`, `*.swp`, `*.tmp`, and `**/.claude/settings.local.json` (Claude Code's per-machine settings, which can carry credentials). It is seeded only if absent — edit it freely, and your version survives recreate — and the `core.excludesFile` wiring is skipped if you already set one.
|
||||||
|
|
||||||
## Secret Scanning
|
## Secret Scanning
|
||||||
|
|
||||||
A [gitleaks](https://github.com/gitleaks/gitleaks) pre-commit hook prevents accidentally committing API keys, passwords, or other secrets.
|
A [gitleaks](https://github.com/gitleaks/gitleaks) pre-commit hook prevents accidentally committing API keys, passwords, or other secrets.
|
||||||
@@ -835,7 +857,11 @@ Container (Debian trixie)
|
|||||||
├── oh-my-opencode-slim (optional — multi-agent orchestration plugin, includes Bun)
|
├── oh-my-opencode-slim (optional — multi-agent orchestration plugin, includes Bun)
|
||||||
├── AWS CLI v2 (SSO + Bedrock auth)
|
├── AWS CLI v2 (SSO + Bedrock auth)
|
||||||
├── neovim 0.12, nano, micro, tmux, htop, bat, eza, zoxide, uv, rustup, make, gcc, g++, rsync
|
├── neovim 0.12, nano, micro, tmux, htop, bat, eza, zoxide, uv, rustup, make, gcc, g++, rsync
|
||||||
├── git, git-crypt, age, gitleaks, ssh, ripgrep, fd, fzf, jq, curl, tree
|
├── git, git-crypt, age, gitleaks, ssh, ripgrep, fd, fzf, jq, yq, curl, tree
|
||||||
|
├── pandoc + typst (Markdown→PDF via --pdf-engine=typst; HTML/etc.)
|
||||||
|
├── graphviz (dot diagram rendering)
|
||||||
|
├── tldr (tealdeer — quick command examples)
|
||||||
|
├── dot-watch (auto-render .dot to PNG on save)
|
||||||
├── Node.js (for MCP servers)
|
├── Node.js (for MCP servers)
|
||||||
├── Bun (optional — included with oh-my-opencode-slim)
|
├── Bun (optional — included with oh-my-opencode-slim)
|
||||||
├── entrypoint.sh (UID adjustment, git config, provider setup)
|
├── entrypoint.sh (UID adjustment, git config, provider setup)
|
||||||
@@ -864,4 +890,4 @@ Container (Debian trixie)
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT — see [`LICENSE`](LICENSE). The MIT license covers this repository's own contents (Dockerfiles, entrypoint scripts, `rootfs/` seeds, CI workflows, docs). The **published container images** additionally *bundle* third-party software, each under its own terms — see [`THIRD_PARTY.md`](THIRD_PARTY.md) for the summary.
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Third-party notices
|
||||||
|
|
||||||
|
opencode-devbox is distributed under the MIT License (see [`LICENSE`](LICENSE)),
|
||||||
|
which covers **this repository's own contents** — the Dockerfiles, entrypoint
|
||||||
|
scripts, `rootfs/` seeds, CI workflows, and docs.
|
||||||
|
|
||||||
|
The **published container images** (`joakimp/opencode-devbox:*`) additionally
|
||||||
|
*bundle* third-party software, each of which remains under its own license. This
|
||||||
|
file is a good-faith summary; the authoritative sources are the upstream
|
||||||
|
projects and, for OS packages, the per-package copyright files inside the image
|
||||||
|
at `/usr/share/doc/<package>/copyright`.
|
||||||
|
|
||||||
|
## opencode and agent components (installed in the variant layer)
|
||||||
|
|
||||||
|
| Component | Upstream | License |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| opencode | github.com/sst/opencode | MIT |
|
||||||
|
| oh-my-opencode-slim (OMOS) *(`-omos` variant only)* | npm | MIT |
|
||||||
|
| mempalace / mempalace-toolkit | authored by the maintainer (Joakim Persson) | MIT |
|
||||||
|
| gitea-mcp | gitea.com/gitea/gitea-mcp | MIT |
|
||||||
|
|
||||||
|
## Tooling baked into the base image
|
||||||
|
|
||||||
|
| Component | Upstream | License (best effort) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| gosu | github.com/tianon/gosu | Apache-2.0 |
|
||||||
|
| Node.js | nodejs.org | MIT (bundles components under their own licenses) |
|
||||||
|
| uv | github.com/astral-sh/uv | Apache-2.0 OR MIT |
|
||||||
|
| Neovim | neovim.io | Apache-2.0 + Vim license |
|
||||||
|
| micro | github.com/zyedidia/micro | MIT |
|
||||||
|
| Pandoc | pandoc.org | GPL-2.0-or-later |
|
||||||
|
| Typst | github.com/typst/typst | Apache-2.0 |
|
||||||
|
| Graphviz | graphviz.org | CPL-1.0 |
|
||||||
|
| ripgrep / fd / bat / eza / zoxide / tealdeer / yq (mikefarah) | respective repos | MIT / Apache-2.0 / Unlicense (varies) |
|
||||||
|
| bun *(`-omos` variant only)* | github.com/oven-sh/bun | MIT |
|
||||||
|
|
||||||
|
## Base OS
|
||||||
|
|
||||||
|
The image is built `FROM` a Debian base and installs packages via `apt`. Debian
|
||||||
|
and its packages are distributed under their respective licenses (GPL, LGPL,
|
||||||
|
MIT, BSD, and others). See each package's copyright file in the image under
|
||||||
|
`/usr/share/doc/<package>/copyright`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Licenses marked "best effort" are widely known but were not each verified at
|
||||||
|
the exact bundled version; consult the upstream project for authoritative
|
||||||
|
terms. Corrections welcome.*
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
" opencode-devbox — system-wide Neovim defaults.
|
||||||
|
"
|
||||||
|
" This is Neovim's *system vimrc*: it loads for every user before any personal
|
||||||
|
" ~/.config/nvim, and personal configs can still override it.
|
||||||
|
"
|
||||||
|
" Enable 24-bit ("true") colour. Without it, Neovim's default theme is squeezed
|
||||||
|
" into a 256-colour palette where strings/comments become a muddy, low-contrast
|
||||||
|
" dark colour — a common complaint over ssh/kitty where COLORTERM often isn't
|
||||||
|
" propagated into the container. Modern terminals (kitty, WezTerm, iTerm2,
|
||||||
|
" Alacritty, ...) all support true colour; the bundled kitty-terminfo also lets
|
||||||
|
" Neovim auto-detect it, but forcing it here guarantees readable colour
|
||||||
|
" regardless of how the terminal type / COLORTERM reach the container.
|
||||||
|
"
|
||||||
|
" Opt out for a session: :set notermguicolors
|
||||||
|
" Override permanently: set your own value in ~/.config/nvim/init.lua
|
||||||
|
if has('termguicolors')
|
||||||
|
set termguicolors
|
||||||
|
endif
|
||||||
@@ -54,6 +54,38 @@ alias gs='git status'
|
|||||||
alias gd='git diff'
|
alias gd='git diff'
|
||||||
alias gl='git log --oneline --graph --decorate -20'
|
alias gl='git log --oneline --graph --decorate -20'
|
||||||
|
|
||||||
|
# ── Host SSH reachability check (once per container lifetime) ─────────────
|
||||||
|
# Warns at first shell startup if the Mac host is not reachable via SSH.
|
||||||
|
# Only runs inside a container, only if the jump key exists, and only once
|
||||||
|
# per container lifetime (/tmp flag is cleared on recreate).
|
||||||
|
_devbox_check_host_ssh() {
|
||||||
|
[ -f "/.dockerenv" ] || return 0
|
||||||
|
local ssh_cfg="$HOME/.ssh-local/config"
|
||||||
|
[ -f "$ssh_cfg" ] || return 0
|
||||||
|
local key_pub="$HOME/.ssh-local/devbox_jump_ed25519.pub"
|
||||||
|
[ -f "$key_pub" ] || return 0
|
||||||
|
local flag="/tmp/.devbox_host_ssh_ok"
|
||||||
|
[ -f "$flag" ] && return 0
|
||||||
|
if ssh -F "$ssh_cfg" \
|
||||||
|
-o BatchMode=yes \
|
||||||
|
-o ConnectTimeout=2 \
|
||||||
|
-o StrictHostKeyChecking=accept-new \
|
||||||
|
mac true 2>/dev/null; then
|
||||||
|
touch "$flag"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local pub_key
|
||||||
|
pub_key=$(cat "$key_pub")
|
||||||
|
printf '\n\033[1;33m⚠ devbox: Mac host not reachable via SSH\033[0m\n'
|
||||||
|
printf ' Some tools use SSH to run commands on the Mac host.\n'
|
||||||
|
printf ' Fix (run both on the Mac):\n\n'
|
||||||
|
printf ' \033[1mStep 1\033[0m System Settings → General → Sharing → Remote Login → ON\n\n'
|
||||||
|
printf ' \033[1mStep 2\033[0m echo '"'"'%s'"'"' >> ~/.ssh/authorized_keys\n' "$pub_key"
|
||||||
|
printf '\n Then open a new shell in the container to verify.\n\n'
|
||||||
|
}
|
||||||
|
_devbox_check_host_ssh
|
||||||
|
unset -f _devbox_check_host_ssh
|
||||||
|
|
||||||
# ── LAN access via the host (dssh) ───────────────────────────────────
|
# ── LAN access via the host (dssh) ───────────────────────────────────
|
||||||
# When running on a VM-backed host (macOS OrbStack / Docker Desktop), the
|
# When running on a VM-backed host (macOS OrbStack / Docker Desktop), the
|
||||||
# entrypoint's setup-lan-access.sh generates ~/.ssh-local/config so the host
|
# entrypoint's setup-lan-access.sh generates ~/.ssh-local/config so the host
|
||||||
|
|||||||
@@ -9,3 +9,7 @@
|
|||||||
*.orig
|
*.orig
|
||||||
*.swp
|
*.swp
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|
||||||
|
# Claude Code per-machine local settings: holds machine-specific permissions
|
||||||
|
# and can carry credentials — must never be committed.
|
||||||
|
**/.claude/settings.local.json
|
||||||
|
|||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# dot-watch — auto-rerender a graphviz .dot file to PNG on every save.
|
||||||
|
#
|
||||||
|
# WHY THIS EXISTS
|
||||||
|
# pi-studio renders mermaid natively but has no graphviz/DOT renderer.
|
||||||
|
# Its markdown preview DOES render local image links (.png/.jpg/.gif/.webp),
|
||||||
|
# and the editor offers "refresh from disk". This helper closes the loop:
|
||||||
|
# edit a .dot file -> dot-watch regenerates <name>.png -> hit refresh in
|
||||||
|
# Studio to see the update. Uses mtime polling (no inotify dependency,
|
||||||
|
# which isn't in the trixie-slim base).
|
||||||
|
#
|
||||||
|
# USAGE
|
||||||
|
# dot-watch <file.dot> [layout] [dpi]
|
||||||
|
# layout: dot|neato|fdp|circo|twopi (default: dot)
|
||||||
|
# dpi: output resolution (default: 150)
|
||||||
|
# env: DOT_WATCH_INTERVAL=<seconds> poll interval (default: 1)
|
||||||
|
#
|
||||||
|
# EXAMPLES
|
||||||
|
# dot-watch /workspace/graph.dot
|
||||||
|
# dot-watch graph.dot neato 200
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SRC="${1:?usage: dot-watch <file.dot> [layout] [dpi]}"
|
||||||
|
LAYOUT="${2:-dot}"
|
||||||
|
DPI="${3:-150}"
|
||||||
|
|
||||||
|
[[ -f "$SRC" ]] || { echo "error: no such file: $SRC" >&2; exit 1; }
|
||||||
|
command -v "$LAYOUT" >/dev/null || { echo "error: layout engine '$LAYOUT' not found" >&2; exit 1; }
|
||||||
|
|
||||||
|
OUT="${SRC%.dot}.png"
|
||||||
|
INTERVAL="${DOT_WATCH_INTERVAL:-1}" # seconds between polls
|
||||||
|
ERRLOG="$(mktemp -t dot-watch.XXXXXX.err)"
|
||||||
|
trap 'rm -f "$ERRLOG"' EXIT
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if "$LAYOUT" -Tpng -Gdpi="$DPI" "$SRC" -o "$OUT" 2> "$ERRLOG"; then
|
||||||
|
printf '[%s] rendered -> %s\n' "$(date +%H:%M:%S)" "$OUT"
|
||||||
|
else
|
||||||
|
printf '[%s] DOT error:\n' "$(date +%H:%M:%S)"
|
||||||
|
sed 's/^/ /' "$ERRLOG"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# portable mtime (GNU stat, fallback to BSD stat)
|
||||||
|
mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null; }
|
||||||
|
|
||||||
|
echo "watching $SRC ($LAYOUT, ${DPI}dpi) -> $OUT [Ctrl-C to stop]"
|
||||||
|
render
|
||||||
|
last="$(mtime "$SRC")"
|
||||||
|
while true; do
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
[[ -f "$SRC" ]] || continue
|
||||||
|
now="$(mtime "$SRC")"
|
||||||
|
if [[ "$now" != "$last" ]]; then
|
||||||
|
last="$now"
|
||||||
|
render
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -41,11 +41,9 @@ from pathlib import Path
|
|||||||
|
|
||||||
# Default model per provider. Update here when upstream changes.
|
# Default model per provider. Update here when upstream changes.
|
||||||
DEFAULT_MODELS: dict[str, str] = {
|
DEFAULT_MODELS: dict[str, str] = {
|
||||||
"anthropic": "anthropic/claude-sonnet-4-6",
|
"anthropic": "anthropic/claude-sonnet-5",
|
||||||
"openai": "openai/gpt-5.4",
|
"openai": "openai/gpt-5.4",
|
||||||
"amazon-bedrock": (
|
"amazon-bedrock": "amazon-bedrock/global.anthropic.claude-sonnet-5",
|
||||||
"amazon-bedrock/global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fallback when OPENCODE_PROVIDER is set but not recognized.
|
# Fallback when OPENCODE_PROVIDER is set but not recognized.
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# xterm-ghostty — alias of the maintained ncurses `ghostty` terminfo entry.
|
||||||
|
#
|
||||||
|
# Ghostty sets TERM=xterm-ghostty by default, but the ncurses terminfo
|
||||||
|
# database (Debian: ncurses-term) ships the entry under the name `ghostty`
|
||||||
|
# only — there is no `xterm-ghostty` alias, and no distro packages one. This
|
||||||
|
# thin alias makes xterm-ghostty resolve to the same upstream-maintained
|
||||||
|
# capability set, so SSH sessions from a Ghostty terminal work without
|
||||||
|
# vendoring Ghostty's full (Zig-generated) terminfo here.
|
||||||
|
#
|
||||||
|
# `use=ghostty` is resolved by `tic` at compile time against the base
|
||||||
|
# `ghostty` entry from ncurses-term (installed in Dockerfile.base before the
|
||||||
|
# compile step). Compiled with `tic -x`.
|
||||||
|
xterm-ghostty|Ghostty terminal emulator (xterm-ghostty alias),
|
||||||
|
use=ghostty,
|
||||||
+29
-2
@@ -8,6 +8,10 @@
|
|||||||
# - Entrypoint runs cleanly as non-root after UID adjustment
|
# - Entrypoint runs cleanly as non-root after UID adjustment
|
||||||
# - Generated opencode.json has the expected shape
|
# - Generated opencode.json has the expected shape
|
||||||
# - MCP wrapper works (when mempalace is installed)
|
# - MCP wrapper works (when mempalace is installed)
|
||||||
|
# - typst PDF engine for pandoc — `pandoc --pdf-engine=typst`
|
||||||
|
# - terminfo for modern emulators: xterm-kitty, xterm-ghostty, wezterm,
|
||||||
|
# alacritty, foot (kitty-terminfo + ncurses-term + compiled ghostty alias)
|
||||||
|
# - Neovim true-colour default (sysinit.vim termguicolors)
|
||||||
#
|
#
|
||||||
# Usage: ./scripts/smoke-test.sh <image> [--variant base|omos]
|
# Usage: ./scripts/smoke-test.sh <image> [--variant base|omos]
|
||||||
#
|
#
|
||||||
@@ -119,6 +123,10 @@ run "node" "node --version"
|
|||||||
run "npm" "npm --version"
|
run "npm" "npm --version"
|
||||||
run "git" "git --version"
|
run "git" "git --version"
|
||||||
run "nvim" "nvim --version | head -1"
|
run "nvim" "nvim --version | head -1"
|
||||||
|
run "nvim true-colour default (sysinit.vim)" "nvim --headless -c 'lua os.exit(vim.o.termguicolors and 0 or 1)'"
|
||||||
|
run "kitty-terminfo" "infocmp -x xterm-kitty >/dev/null 2>&1"
|
||||||
|
run "terminfo: modern emulators (ncurses-term)" 'for t in wezterm alacritty foot ghostty st-256color; do infocmp -x "$t" >/dev/null 2>&1 || exit 1; done'
|
||||||
|
run "terminfo: xterm-ghostty alias (tic)" "infocmp -x xterm-ghostty >/dev/null 2>&1"
|
||||||
run "nano" "nano --version | head -1"
|
run "nano" "nano --version | head -1"
|
||||||
run "micro" "micro --version"
|
run "micro" "micro --version"
|
||||||
run "bat" "bat --version"
|
run "bat" "bat --version"
|
||||||
@@ -131,12 +139,24 @@ run "fzf" "fzf --version"
|
|||||||
run "fd" "fd --version"
|
run "fd" "fd --version"
|
||||||
run "rg" "rg --version | head -1"
|
run "rg" "rg --version | head -1"
|
||||||
run "jq" "jq --version"
|
run "jq" "jq --version"
|
||||||
|
run "yq" "yq --version"
|
||||||
run "git-crypt" "git-crypt --version | head -1"
|
run "git-crypt" "git-crypt --version | head -1"
|
||||||
run "gitleaks" "gitleaks version"
|
run "gitleaks" "gitleaks version"
|
||||||
run "aws" "aws --version"
|
run "aws" "aws --version"
|
||||||
run "gitea-mcp" "gitea-mcp --version"
|
run "gitea-mcp" "gitea-mcp --version"
|
||||||
run "gosu" "gosu --version"
|
run "gosu" "gosu --version"
|
||||||
run "tmux" "tmux -V"
|
run "tmux" "tmux -V"
|
||||||
|
run "pandoc" "pandoc --version | head -1"
|
||||||
|
run "typst" "typst --version"
|
||||||
|
run "pandoc+typst PDF engine" "printf '# hi\n' | pandoc --pdf-engine=typst -o /tmp/_smoke.pdf - && test -s /tmp/_smoke.pdf; rm -f /tmp/_smoke.pdf"
|
||||||
|
run "graphviz (dot)" "dot -V"
|
||||||
|
run "tldr (tealdeer)" "tldr --version"
|
||||||
|
run "dot-watch" "test -x /usr/local/bin/dot-watch && bash -n /usr/local/bin/dot-watch && echo ok"
|
||||||
|
|
||||||
|
# Background subagents: opencode gates them behind this experimental env var,
|
||||||
|
# and OMOS V2+ default orchestration depends on it. Baked ON as an ENV in
|
||||||
|
# Dockerfile.base — assert it's present in the image environment (both variants).
|
||||||
|
run_expect "bg-subagents env baked" "printenv OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS" "true"
|
||||||
|
|
||||||
# SSH ControlMaster baked defaults: the config file must exist (image-level)
|
# SSH ControlMaster baked defaults: the config file must exist (image-level)
|
||||||
# and ssh -G must report ControlPath rooted at /tmp/sshcm/ for an arbitrary
|
# and ssh -G must report ControlPath rooted at /tmp/sshcm/ for an arbitrary
|
||||||
@@ -376,8 +396,15 @@ echo " Uncompressed size: ${SIZE_MB} MB"
|
|||||||
# ~250 MB headroom avoids a partial-publish + letter-suffix recovery cycle.
|
# ~250 MB headroom avoids a partial-publish + letter-suffix recovery cycle.
|
||||||
# CI's smoke size print + resolved-versions table records the actual landed
|
# CI's smoke size print + resolved-versions table records the actual landed
|
||||||
# sizes; tighten later if they come in low.
|
# sizes; tighten later if they come in low.
|
||||||
THRESHOLD=2750
|
# v2.6.0: bumped +200 MB (2750->2950 base, 3450->3650 omos). The pi-devbox
|
||||||
[ "$VARIANT" = "omos" ] && THRESHOLD=3450
|
# CLI-parity additions (pandoc ~150 MB + graphviz ~40 MB + tealdeer ~5 MB, all
|
||||||
|
# on the base layer) consumed almost all of the ~250 MB headroom v1.16.2 had
|
||||||
|
# restored — v2.6.0 passed 2750/3450 but with a slim margin. Restoring ~250 MB
|
||||||
|
# headroom keeps the guardrail catching *runaway* growth (accidental texlive/
|
||||||
|
# chrome bake-in) rather than tripping on routine apt drift or a minor opencode
|
||||||
|
# bump. smoke still prints the actual landed size each run; tighten if low.
|
||||||
|
THRESHOLD=2950
|
||||||
|
[ "$VARIANT" = "omos" ] && THRESHOLD=3650
|
||||||
if [ "$SIZE_MB" -gt "$THRESHOLD" ]; then
|
if [ "$SIZE_MB" -gt "$THRESHOLD" ]; then
|
||||||
fail "image size ${SIZE_MB} MB exceeds threshold ${THRESHOLD} MB for variant=$VARIANT"
|
fail "image size ${SIZE_MB} MB exceeds threshold ${THRESHOLD} MB for variant=$VARIANT"
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user