bootstrap failed outright on Oracle Linux: docker/install.sh piped
get.docker.com to sh, and that script allowlists distros by /etc/os-release
ID, so it aborted with "ERROR: Unsupported distribution 'ol'".
On a distro it does recognize, the convenience script only adds the repo and
installs the packages -- exactly what install_rhel() now does directly. Doing
it ourselves drops the allowlist, so every EL rebuild (Oracle, Rocky, Alma,
CentOS, RHEL) goes through one code path instead of needing to be enumerated.
Debian-family still uses get.docker.com. containerd.io needs --allowerasing to
replace the runc that Oracle Linux ships.
Also stop one failed module from killing the whole run: modules were bare
calls under `set -eu`, so docker/'s exit meant bin/, node/ and ansible/
silently never ran -- an unsupported distro string left the box with no CLIs,
no npx and no ansible. run_module warns and continues, then bootstrap exits
non-zero with a summary of what failed. Same guard 35befbe added for
individual tools in bin/install.sh, one level up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/v1/models returns ONLY model ids, so every model was deployed as {} and
opencode ran all of them on defaults -- no context window (so it cannot manage
truncation), no cost, and no idea whether the model can call tools. opencode
requires this metadata in the models map: it has no /v1/models discovery of its
own, and models.dev only covers known providers, not a private LiteLLM gateway.
sync.sh now also queries /model/info and maps LiteLLM's metadata onto opencode's
per-model spec:
max_input_tokens -> limit.context
max_output_tokens -> limit.output
input/output_cost_per_token -> cost.input/output (per-token -> $ per 1M)
supports_function_calling -> tool_call
supports_reasoning -> reasoning
supports_vision -> attachment
Best-effort and backwards-compatible: /model/info is outside the llm_api_routes
scope most virtual keys have, so an un-updated key 403s and falls back to
names-only -- exactly today's behaviour, no regression.
Note the gateway only reports metadata for models in LiteLLM's registry. The
self-hosted vLLM entries (qwen3-*) carry no model_info in the gateway's
model_list, so they still deploy bare. Fixing that is a gateway-side change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sync runs only at shell start and is throttled to MCP_SYNC_TTL (1h), so a
model added on the gateway can take up to an hour AND a new terminal before
opencode sees it. There was no way to say "fetch it now", so the recurring
symptom was "I added a model and it isn't showing up" -- with a healthy
gateway and a correctly-scoped key.
`mcp-sync` re-sources the hook with the throttle disabled and prints the
resulting model list, so it also answers "what does the client actually have?"
in one step.
README gains a section splitting the two failure modes: a model the gateway
returns but opencode lacks is a stale client; a model the gateway does not
return is a LiteLLM model_list / key-scoping problem.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a minimal Debian/RHEL image neither python3 nor unzip is present.
lib/fetch.sh's install_zip_bin needs one of them, so the 1Password CLI
install failed -- and under `set -eu` that killed bin/install.sh outright.
install_1p sits immediately before install_opencode and install_zarf, so
neither installed; bin/install.sh's non-zero exit then aborted bootstrap.sh,
so node/ (npx) and ansible/ (python3) never ran either.
Net effect on a fresh box: no opencode, no npx for the MCP servers, and no
python3 for model discovery. It presented as "opencode has no models or
skills". Reproduced in a clean debian:13 container.
Two fixes:
- bin/install.sh: each tool now installs via try(), which warns and
continues instead of aborting, and reports a summary of what failed. One
optional CLI is not worth the rest of the bootstrap. The subshell
re-enables set -e so a failed download can't fall through to mark_version
and stamp a version that was never installed.
- bootstrap.sh: ensure python3 before bin/ runs. It is a hard dependency,
not an extra -- lib/fetch.sh needs it (or unzip) for .zip assets, and
mcp-config/sync.sh needs it to inject discovered models.
Verified end to end on fresh debian:13: bootstrap completes, opencode/zarf/
op/npx/node/python3 all present, and opencode resolves all 3 covert-gpt
models.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
~/.zshrc is MANAGED: zsh/install.sh overwrites it wholesale (cp) on every
bootstrap run. install.sh appended the mcp-sync hook to that same file, so
the next bootstrap silently deleted it -- sync.sh then never ran, so
OPENCODE_CONFIG was never exported and opencode came up with no models and
no skills. It looked like a broken gateway or a bad model; it was a wiped
shell hook.
~/.zshrc.local is the unmanaged file the managed zshrc sources last, so the
hook now survives a re-bootstrap. Verified: hook lands in ~/.zshrc.local
(0600), managed ~/.zshrc is left untouched, and OPENCODE_CONFIG still
exports after simulating the bootstrap overwrite.
Also warn instead of failing silently when python3 is missing: sync.sh then
deploys the base config verbatim with an EMPTY models map, which presents as
"the gateway has no models" rather than "a dependency is absent".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Five skills covering this repo's own conventions: adding a bootstrap
module, adding a CLI to bin/, bumping a pinned version, adding an MCP
server, and verifying a module change.
Lives in .agents/skills/ because opencode discovers that path natively
alongside .opencode/skills/ and .claude/skills/, making it the most
tool-neutral of the three. Distinct from the repo's skills/ tree, which
ships to every covert-gpt user via sync.sh; these stay in the checkout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
zarf v0.81.0 (current latest), a single static binary for offline/airgapped
Kubernetes. Both Linux release assets verified to resolve; install is
idempotent via the ~/.local/bin/.versions stamp.
Also drops a stray leading space from install_opencode's argument line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every API worth exposing (Gitea, Proxmox) is user-scoped, so hosting MCP
centrally means one shared token per server: everyone acts as the same
identity and attribution is lost. LiteLLM's per-request BYOK cannot cover
all of them either -- it builds its tool->server map from a headerless
background listing, and gitea-mcp hard-exits without a token, so it never
registers. A split brain (some MCPs server-side, some client-side) is worse
than either, so all of them move client-side.
Servers are declared local, spawned via npx with pinned versions, and read
each user's OWN credentials via {env:VAR}. No token is ever written to the
config; the file stays identical for every user.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MCP servers in mcp-config/ are npm packages that opencode spawns as
`npx -y <pkg>`. Without npx on PATH they all fail to start, and opencode
reports that only as a dead server -- easy to misread as a broken config.
Node is a tree, not a single binary, so it follows the ansible module:
unpack under ~/.local/opt and symlink the CLIs into ~/.local/bin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Models and MCP servers are discovered from the LiteLLM gateway at sync time
(/v1/models, and the aggregate /mcp/ endpoint). Skills have no gateway side to
query: LiteLLM's /v1/skills is a passthrough to Anthropic's Skills API, which
lists skills on an Anthropic account rather than serving skill files, and
/public/skill_hub is a marketplace catalog. So the repo is the source of truth
and the tree is fetched whole, from the repo tarball.
Deploy to ~/.config/mcp-sync/skills rather than ~/.config/opencode/skills so
the atomic replace can never delete a skill the user wrote by hand; opencode
picks the tree up via skills.paths, which expands {env:HOME} — verified — so
the shared config stays identical for every user.
Same throttle and non-fatal discipline as the config sync: a fetch that yields
no skills/ leaves the last-good tree in place instead of emptying it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers the docker/ module, task/op/opencode in bin/, the kind gating
change, and the new per-machine secrets section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two changes to the synced opencode config:
- Model provider. opencode can't list models from a custom OpenAI-compatible
provider itself, so sync.sh queries the gateway's /v1/models with the
user's key and materializes the map client-side. New gateway models land
on every box at the next sync with no config edit. If discovery fails the
previous map is carried over, so a gateway hiccup never empties the list.
- Aggregate MCP endpoint. One entry at /mcp/ (trailing slash required — the
bare path 307-redirects and MCP clients don't follow it) exposes every
server the key can access, replacing the per-server gitea entry.
sync.sh also exports OPENCODE_CONFIG: opencode does not auto-load extra
files from its config dir, so without it the synced file was ignored.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
~/.zshrc is managed and overwritten on every bootstrap, so there was
nowhere to put a machine-local key. The managed rc now sources
~/.zshrc.local last (so it can override anything) and bootstrap never
touches that file. zshrc.local.example documents the variables modules
look for: GITEA_KEY, LITELLM_KEY, OP_SERVICE_ACCOUNT_TOKEN.
Secrets stay out of the repo — zsh/zshrc is shared config applied to
every box.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
op is shipped as a zip rather than a tarball, so lib/fetch.sh grows an
install_zip_bin helper (unzip, falling back to python3's zipfile so
neither is a hard dependency).
Credentials for both stay per-box and out of this repo: op signs in via
`op signin` or OP_SERVICE_ACCOUNT_TOKEN, and opencode reaches the
covert-gpt gateway with the key that mcp-config/ syncs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kind never worked reliably against rootless Docker on Kasm, even with the
cgroup delegation from docker-rootless-cgroup/. So install full Docker
where we can, and flip bin/'s kind gate from "rootless is available" to
"a rootful daemon answers" — kind is simply skipped on Kasm now.
docker/install.sh no-ops when a rootless daemon is already running so it
never fights Kasm's setup. It runs before bin/ in bootstrap so the kind
gate can see the daemon it just started. zsh/zshrc re-execs via newgrp
when the account gained the docker group but the live shell predates it.
docker-rootless-cgroup/ keeps its delegation (still useful for
per-container resource limits); only its kind-centric comments change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pinned to v3.52.0, installed as a static release binary into ~/.local/bin
like the other tools. The release tarball has the binary at its root, so
the existing install_tarball_bin helper covers it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rootless Docker only gets cgroup v2 controllers delegated when systemd
sets Delegate=yes on the user session. Kasm images have no systemd, so
the daemon comes up with CgroupDriver=none and `kind create cluster`
fails. This module reproduces that delegation by hand: evacuates the
cgroup-namespace root, delegates controllers into a user-owned /rootless
slice, installs a PATH shim that gives the daemon a private cgroup leaf
so it can self-evacuate, and restarts it. Guarded to no-op on systemd,
rootful, and non-Docker hosts; wired into bootstrap.sh last.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kind needs a container runtime to create clusters, so only install it
when a rootless Docker daemon is actually reachable (SecurityOptions
reports name=rootless). Otherwise skip with a note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the placeholder ripgrep/fd/bat/fzf set with devops tooling, keeping
jq: kubectl, helm, k9s, kubectx/kubens, stern, yq, OpenTofu, and terragrunt
— all self-contained, statically linked release binaries in ~/.local/bin,
so they persist across Kasm redeploys with no reinstall.
Switch idempotency from parsing each tool's --version (kubectl/helm/k9s have
no uniform flag) to a version stamp under ~/.local/bin/.versions, and add
fetch_arch_deb() for the amd64/arm64 naming most devops tools use.
Ansible is Python, not a single binary, so it gets its own module: a venv at
~/.local/opt/ansible with its CLIs symlinked into ~/.local/bin. The guard
verifies ansible actually runs (a redeploy can wipe the system python the
venv links to) and rebuilds if not.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New bin/install.sh downloads self-contained release binaries (ripgrep, fd,
bat, fzf, jq) into ~/.local/bin — no sudo, nothing outside $HOME, so they
persist across Kasm redeploys with no reinstall. Statically linked where
available, so they keep working even after the root fs is wiped. Idempotent:
pinned versions already present are skipped.
lib/fetch.sh adds arch detection + tarball/raw binary download helpers.
zsh/zshrc puts ~/.local/bin on PATH; bootstrap.sh runs the module.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Piping bootstrap.sh into sh left $0 as a non-path, so SCRIPT_DIR pointed
at the caller's cwd and the module scripts couldn't be found (exit 127).
When no checkout is present, clone/update the repo into ~/.dotfiles and
re-exec from there. Add a Quick start section to the README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cross-distro (RHEL/Debian) VM setup: zsh + oh-my-zsh with a self-healing
bash fallback for environments that reset the root filesystem or hardcode
bash for new terminals (e.g. Kasm Workspaces), plus git identity and
HTTPS credential-store configuration.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>