Compare commits
6
Commits
eff01b1b0b
...
c0df3a1596
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0df3a1596 | ||
|
|
16e01cb3d0 | ||
|
|
efca892109 | ||
|
|
d9e3ca2565 | ||
|
|
3751836c5c | ||
|
|
ddeb73fd1c |
@@ -26,18 +26,35 @@ Already have a clone? Just run it directly:
|
||||
## Modules
|
||||
|
||||
- `zsh/` — installs zsh + oh-my-zsh, drops a managed `~/.zshrc`
|
||||
(robbyrussell theme, git plugin), and sets zsh as the login shell.
|
||||
(robbyrussell theme, git plugin), and sets zsh as the login shell. The
|
||||
managed rc sources `~/.zshrc.local` last for per-machine customizations
|
||||
(see below).
|
||||
- `git/` — sets git `user.name`/`user.email` and configures
|
||||
`credential.helper store` (plaintext `~/.git-credentials`, 0600) for
|
||||
HTTPS auth.
|
||||
- `bin/` — installs userland devops CLIs (jq, kubectl, helm, k9s,
|
||||
kubectx/kubens, stern, yq, OpenTofu, terragrunt, plus kind when rootless
|
||||
Docker is available) as self-contained,
|
||||
statically linked release binaries into `~/.local/bin`. No sudo, nothing
|
||||
outside `$HOME`, so they persist across Kasm redeploys with no reinstall.
|
||||
Idempotent: each tool records its version under `~/.local/bin/.versions`
|
||||
and is skipped when the pinned version is already present. `zsh/zshrc`
|
||||
puts `~/.local/bin` on `PATH`.
|
||||
- `docker/` — installs full (rootful) Docker Engine via get.docker.com,
|
||||
enables the service (systemd hosts), and adds the invoking user to the
|
||||
`docker` group (takes effect at next login). No-ops where a rootless
|
||||
Docker daemon is already running (e.g. Kasm) so it never fights that
|
||||
setup. Uses sudo.
|
||||
- `bin/` — installs userland devops CLIs as self-contained, statically
|
||||
linked release binaries into `~/.local/bin`: jq, kubectl, helm, k9s,
|
||||
kubectx/kubens, stern, yq, OpenTofu, terragrunt, task (go-task, the
|
||||
Taskfile runner), the 1Password CLI `op` for secrets storage, and the
|
||||
opencode coding agent. kind is installed
|
||||
only when full (non-rootless) Docker is available — it doesn't work
|
||||
against rootless Docker, so it is skipped on Kasm. No sudo, nothing
|
||||
outside `$HOME`, so tools persist across Kasm redeploys with no
|
||||
reinstall. Idempotent: each tool records its version under
|
||||
`~/.local/bin/.versions` and is skipped when the pinned version is
|
||||
already present. `zsh/zshrc` puts `~/.local/bin` on `PATH`.
|
||||
|
||||
opencode is configured by `mcp-config/`: it points at the covert-gpt
|
||||
LiteLLM gateway (`https://llm.covert-gpt.com/v1` for models,
|
||||
`https://llm.covert-gpt.com/mcp` for MCP tools). Run
|
||||
`mcp-config/install.sh` once to store your LiteLLM key and enable
|
||||
config sync — without it, opencode installs but can't reach the
|
||||
gateway.
|
||||
- `ansible/` — installs Ansible into a virtualenv at `~/.local/opt/ansible`
|
||||
(it's Python, not a single binary) and symlinks its CLIs into
|
||||
`~/.local/bin`. Persists across redeploys; only rebuilt when the venv is
|
||||
@@ -45,13 +62,37 @@ Already have a clone? Just run it directly:
|
||||
the system package manager (the one place this module uses sudo).
|
||||
- `docker-rootless-cgroup/` — on systemd-less hosts (e.g. Kasm), hand-delegates
|
||||
cgroup v2 controllers to the rootless Docker daemon so per-container cgroups
|
||||
work and `kind` can create clusters (rootless Docker otherwise comes up with
|
||||
work (rootless Docker otherwise comes up with
|
||||
`CgroupDriver=none`). Uses sudo for host cgroup writes and restarts the
|
||||
daemon; no-ops on systemd or non-rootless hosts. Re-run at each bootstrap
|
||||
(the cgroup layout resets on redeploy); its PATH shim lives in `$HOME`.
|
||||
- `mcp-config/` — shared MCP gateway config sync for laptops (LiteLLM /
|
||||
opencode). Independent of `bootstrap.sh`; see its own README.
|
||||
|
||||
## Per-machine customizations and secrets
|
||||
|
||||
`~/.zshrc` is managed — bootstrap overwrites it, so never hand-edit it.
|
||||
Per-machine settings and secrets go in `~/.zshrc.local`, which the managed
|
||||
rc sources last (so it can override anything) and bootstrap never touches:
|
||||
|
||||
```sh
|
||||
cp zsh/zshrc.local.example ~/.zshrc.local && chmod 600 ~/.zshrc.local
|
||||
```
|
||||
|
||||
Variables modules look for:
|
||||
|
||||
- `GITEA_KEY` — personal access token for `git.nic-oconnor.com` (API/CLI
|
||||
use; HTTPS git auth itself goes through `git/`'s credential helper).
|
||||
- `LITELLM_KEY` — covert-gpt gateway key used by opencode. Normally you
|
||||
don't set this by hand: run `mcp-config/install.sh` once and its shell
|
||||
hook exports it from `~/.config/mcp-sync/key`. Set it in `~/.zshrc.local`
|
||||
only on a box without mcp-sync.
|
||||
- `OP_SERVICE_ACCOUNT_TOKEN` — 1Password service-account token for
|
||||
non-interactive `op read` / `op run`.
|
||||
|
||||
Secrets never go in this repo or in `zsh/zshrc` — that file is shared
|
||||
config applied to every box.
|
||||
|
||||
`lib/os-detect.sh` provides shared package-manager detection (`dnf`/`yum`
|
||||
vs `apt`); `lib/fetch.sh` provides arch detection and binary-download
|
||||
helpers. Both are used by module install scripts.
|
||||
|
||||
+54
-11
@@ -20,13 +20,15 @@ REPO_ROOT="$(dirname -- "$SCRIPT_DIR")"
|
||||
ARCH="$(fetch_arch)" # x86_64 / aarch64
|
||||
DEB="$(fetch_arch_deb)" # amd64 / arm64
|
||||
|
||||
# docker_rootless_available — true only if a rootless Docker daemon is up and
|
||||
# reachable. kind needs a container runtime to create clusters; on Kasm we run
|
||||
# Docker rootless (never system Docker), so kind is worth installing only when
|
||||
# that daemon answers. SecurityOptions lists name=rootless under rootless mode.
|
||||
docker_rootless_available() {
|
||||
# docker_full_available — true only if a non-rootless (full) Docker daemon is
|
||||
# up and reachable. kind needs full Docker: we could not get it working against
|
||||
# rootless Docker on Kasm, so it is only worth installing where a rootful
|
||||
# daemon answers. SecurityOptions lists name=rootless under rootless mode.
|
||||
docker_full_available() {
|
||||
command -v docker >/dev/null 2>&1 || return 1
|
||||
docker info -f '{{println .SecurityOptions}}' 2>/dev/null | grep -q 'name=rootless'
|
||||
_secopts="$(docker info -f '{{println .SecurityOptions}}' 2>/dev/null)" || return 1
|
||||
[ -n "$_secopts" ] || return 1
|
||||
! printf '%s' "$_secopts" | grep -q 'name=rootless'
|
||||
}
|
||||
|
||||
# jq — command-line JSON processor (single static binary).
|
||||
@@ -68,9 +70,8 @@ install_k9s() {
|
||||
}
|
||||
|
||||
# kind — run local Kubernetes clusters in Docker (single static binary). Gated
|
||||
# on rootless Docker at the call site since it is useless without a runtime.
|
||||
# On systemd-less hosts the rootless daemon also needs cgroup delegation before
|
||||
# kind can create clusters; see the docker-rootless-cgroup/ module.
|
||||
# on full (non-rootless) Docker at the call site: kind could not be made to
|
||||
# work against rootless Docker on Kasm, so it is skipped there.
|
||||
install_kind() {
|
||||
ver=v0.32.0
|
||||
have_version kind "$ver" && { fsay "kind $ver already installed."; return 0; }
|
||||
@@ -126,6 +127,45 @@ install_tofu() {
|
||||
mark_version tofu "$ver"
|
||||
}
|
||||
|
||||
# op — 1Password CLI, used for secrets storage/retrieval (op read, op run,
|
||||
# op inject). Single static binary shipped as a zip. Sign in with
|
||||
# `op signin` or a service-account token (OP_SERVICE_ACCOUNT_TOKEN) —
|
||||
# credentials are per-box and never live in this repo.
|
||||
install_1p() {
|
||||
ver=2.34.1
|
||||
have_version op "$ver" && { fsay "op $ver already installed."; return 0; }
|
||||
install_zip_bin \
|
||||
"https://cache.agilebits.com/dist/1P/op2/pkg/v$ver/op_linux_${DEB}_v$ver.zip" \
|
||||
op op
|
||||
mark_version op "$ver"
|
||||
}
|
||||
|
||||
# opencode — terminal AI coding agent. Points at the covert-gpt LiteLLM
|
||||
# gateway via mcp-config/opencode.gateway.json (synced by mcp-config/).
|
||||
# Release assets use x64/arm64 tokens rather than amd64/x86_64.
|
||||
install_opencode() {
|
||||
ver=v1.17.18
|
||||
have_version opencode "$ver" && { fsay "opencode $ver already installed."; return 0; }
|
||||
case "$DEB" in
|
||||
amd64) _oc_arch=x64 ;;
|
||||
*) _oc_arch=arm64 ;;
|
||||
esac
|
||||
install_tarball_bin \
|
||||
"https://github.com/sst/opencode/releases/download/$ver/opencode-linux-$_oc_arch.tar.gz" \
|
||||
opencode opencode
|
||||
mark_version opencode "$ver"
|
||||
}
|
||||
|
||||
# task — go-task, the Taskfile.yml task runner. Binary is at the archive root.
|
||||
install_task() {
|
||||
ver=v3.52.0
|
||||
have_version task "$ver" && { fsay "task $ver already installed."; return 0; }
|
||||
install_tarball_bin \
|
||||
"https://github.com/go-task/task/releases/download/$ver/task_linux_$DEB.tar.gz" \
|
||||
task task
|
||||
mark_version task "$ver"
|
||||
}
|
||||
|
||||
# terragrunt — thin wrapper around tofu/terraform (single static binary).
|
||||
install_terragrunt() {
|
||||
ver=v1.1.0
|
||||
@@ -140,16 +180,19 @@ install_jq
|
||||
install_kubectl
|
||||
install_helm
|
||||
install_k9s
|
||||
if docker_rootless_available; then
|
||||
if docker_full_available; then
|
||||
install_kind
|
||||
else
|
||||
fsay "Skipping kind: rootless Docker not available."
|
||||
fsay "Skipping kind: full (non-rootless) Docker not available."
|
||||
fi
|
||||
install_kubectx
|
||||
install_kubens
|
||||
install_stern
|
||||
install_yq
|
||||
install_tofu
|
||||
install_task
|
||||
install_terragrunt
|
||||
install_1p
|
||||
install_opencode
|
||||
|
||||
fsay "Done. Tools in $BIN_DIR (added to PATH by zsh/zshrc)."
|
||||
|
||||
@@ -31,6 +31,8 @@ fi
|
||||
|
||||
"$SCRIPT_DIR/zsh/install.sh"
|
||||
"$SCRIPT_DIR/git/install.sh"
|
||||
# Runs before bin/ so the kind install gate can see the rootful daemon.
|
||||
"$SCRIPT_DIR/docker/install.sh"
|
||||
"$SCRIPT_DIR/bin/install.sh"
|
||||
"$SCRIPT_DIR/ansible/install.sh"
|
||||
# Runs last: needs rootless Docker already up (started at boot) so it can
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#!/usr/bin/env sh
|
||||
# docker-rootless-cgroup/install.sh — give rootless Docker cgroup v2 controller
|
||||
# delegation on a systemd-less host (e.g. Kasm), so per-container cgroups work
|
||||
# and `kind` can create clusters.
|
||||
# delegation on a systemd-less host (e.g. Kasm), so per-container cgroups work.
|
||||
# (This was originally done to support `kind`, but kind never worked reliably
|
||||
# against rootless Docker even with delegation — bin/ now only installs kind
|
||||
# where full Docker is available. The delegation itself is still useful for
|
||||
# per-container resource limits.)
|
||||
#
|
||||
# Why: rootless Docker only receives delegated cgroup v2 controllers when
|
||||
# systemd sets `Delegate=yes` on the user session. Kasm images run no systemd
|
||||
@@ -117,7 +120,7 @@ cat > "$tmp" <<'SH'
|
||||
#!/bin/sh
|
||||
# Managed by dotfiles (docker-rootless-cgroup). Shadows /usr/bin/dockerd-rootless.sh
|
||||
# on PATH. Places the rootless Docker daemon in a private, delegated cgroup v2
|
||||
# leaf so it can enable controllers (needed by `kind`), then execs the real
|
||||
# leaf so it can enable controllers (per-container cgroups), then execs the real
|
||||
# launcher. No-op if the delegated slice is absent, so it is safe anywhere.
|
||||
_cg=/sys/fs/cgroup/rootless/daemon
|
||||
if [ -w /sys/fs/cgroup/rootless ] && [ -d "$_cg" ]; then
|
||||
@@ -156,9 +159,9 @@ done
|
||||
driver="$(docker info -f '{{.CgroupDriver}}' 2>/dev/null || true)"
|
||||
if [ "$driver" = "none" ] || [ -z "$driver" ]; then
|
||||
say "docker-rootless-cgroup: WARNING — CgroupDriver is still '${driver:-unreachable}'."
|
||||
say " kind will not work yet. Inspect the new dockerd's cgroup:"
|
||||
say " Per-container cgroups will not work yet. Inspect the new dockerd's cgroup:"
|
||||
say " cat /proc/\$(pgrep -x dockerd | head -1)/cgroup # expect .../rootless/daemon"
|
||||
exit 0
|
||||
fi
|
||||
say "docker-rootless-cgroup: done. CgroupDriver=$driver — rootless Docker now has"
|
||||
say " delegated cgroups; \`kind create cluster\` can run."
|
||||
say " delegated cgroups; per-container resource limits work."
|
||||
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env sh
|
||||
# docker/install.sh — install full (rootful) Docker Engine and let the
|
||||
# invoking user talk to it without sudo.
|
||||
#
|
||||
# What it does:
|
||||
# 1. Installs Docker Engine via Docker's official convenience script
|
||||
# (get.docker.com — supports both RHEL- and Debian-family distros and
|
||||
# sets up the upstream package repo). Skipped when `docker` is already
|
||||
# installed.
|
||||
# 2. Enables and starts the service (systemd hosts only).
|
||||
# 3. Adds the invoking user to the `docker` group so the daemon socket is
|
||||
# usable without sudo. Takes effect at next login.
|
||||
#
|
||||
# Deliberately no-ops when a rootless Docker daemon is detected (e.g. Kasm
|
||||
# ships its own rootless setup) — installing rootful Docker there would fight
|
||||
# the existing runtime. See docker-rootless-cgroup/ for the Kasm-side config.
|
||||
#
|
||||
# Idempotent and safe to re-run. Uses sudo.
|
||||
set -eu
|
||||
|
||||
say() { printf '%s\n' "$*" >&2; }
|
||||
|
||||
# --- skip on rootless-Docker hosts -------------------------------------------
|
||||
if command -v docker >/dev/null 2>&1 &&
|
||||
docker info -f '{{println .SecurityOptions}}' 2>/dev/null | grep -q 'name=rootless'; then
|
||||
say "docker: rootless Docker daemon detected; leaving it alone."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- 1. install engine -------------------------------------------------------
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
say "docker: engine already installed ($(docker --version 2>/dev/null || echo 'version unknown'))."
|
||||
else
|
||||
say "docker: installing Docker Engine via get.docker.com..."
|
||||
tmp="$(mktemp)"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL https://get.docker.com -o "$tmp"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO "$tmp" https://get.docker.com
|
||||
else
|
||||
say "docker: need curl or wget"; exit 1
|
||||
fi
|
||||
sudo sh "$tmp"
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
|
||||
# --- 2. enable + start the service (systemd only) ----------------------------
|
||||
if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then
|
||||
sudo systemctl enable --now docker
|
||||
else
|
||||
say "docker: no systemd; not managing the service (start dockerd yourself)."
|
||||
fi
|
||||
|
||||
# --- 3. group membership for the invoking user -------------------------------
|
||||
user="$(id -un)"
|
||||
if id -nG "$user" | tr ' ' '\n' | grep -qx docker; then
|
||||
say "docker: $user already in the docker group."
|
||||
else
|
||||
sudo usermod -aG docker "$user"
|
||||
say "docker: added $user to the docker group."
|
||||
say " Log out and back in (or run 'newgrp docker') for it to take effect."
|
||||
fi
|
||||
@@ -73,6 +73,26 @@ install_tarball_bin() {
|
||||
fsay "installed $BIN_DIR/$3"
|
||||
}
|
||||
|
||||
# install_zip_bin <url> <path-within-archive> <dest-name> — like
|
||||
# install_tarball_bin but for .zip assets (e.g. 1Password CLI). Uses unzip
|
||||
# when present, else python3's zipfile module, so no hard dependency on
|
||||
# either being installed.
|
||||
install_zip_bin() {
|
||||
mkdir -p "$BIN_DIR"
|
||||
_tmp="$(mktemp -d)"
|
||||
_fetch_dl "$1" "$_tmp/archive.zip"
|
||||
if command -v unzip >/dev/null 2>&1; then
|
||||
unzip -q "$_tmp/archive.zip" -d "$_tmp"
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
python3 -m zipfile -e "$_tmp/archive.zip" "$_tmp"
|
||||
else
|
||||
fsay "fetch: need unzip or python3 to extract zip"; rm -rf "$_tmp"; return 1
|
||||
fi
|
||||
install -m 0755 "$_tmp/$2" "$BIN_DIR/$3"
|
||||
rm -rf "$_tmp"
|
||||
fsay "installed $BIN_DIR/$3"
|
||||
}
|
||||
|
||||
# install_raw_bin <url> <dest-name> — download a single raw binary.
|
||||
install_raw_bin() {
|
||||
mkdir -p "$BIN_DIR"
|
||||
|
||||
+32
-15
@@ -10,18 +10,33 @@ sync — the laptops pull from Gitea's `main`.)
|
||||
|
||||
## How it works
|
||||
|
||||
- `opencode.gateway.json` — the shared server list. **Identical for every
|
||||
user**: the per-user key is injected via `{env:LITELLM_KEY}`, never written
|
||||
into the file. Add a gateway server here and everyone picks it up next shell.
|
||||
- `sync.sh` — sourced from each user's shell rc. On a new shell it pulls
|
||||
`opencode.gateway.json` (throttled ~1h), drops it where opencode merges it,
|
||||
and exports `LITELLM_KEY` from `~/.config/mcp-sync/key`. Silent + non-fatal:
|
||||
a Gitea hiccup never breaks the shell, last-good config stays in place.
|
||||
- `opencode.gateway.json` — the shared base config: the `covert-gpt` model
|
||||
provider (LiteLLM's OpenAI-compatible endpoint at
|
||||
`https://llm.covert-gpt.com/v1`, models map left empty — it is filled by
|
||||
discovery at sync time) and a single MCP entry for the gateway's
|
||||
**aggregate** endpoint `https://llm.covert-gpt.com/mcp/` (trailing slash
|
||||
required; the bare path 307-redirects, which MCP clients don't follow).
|
||||
That one endpoint exposes every MCP server the user's key can access, so
|
||||
new gateway servers need no client config change at all. **Identical for
|
||||
every user**: the per-user key is injected via `{env:LITELLM_KEY}`, never
|
||||
written into the file.
|
||||
- `sync.sh` — sourced from each user's shell rc. On a new shell (throttled
|
||||
~1h) it pulls the base config from Gitea, queries the gateway's
|
||||
`/v1/models` with the user's key to **auto-discover models** (opencode
|
||||
can't list models from a custom provider itself, so the map is
|
||||
materialized client-side; if discovery fails the previous map is carried
|
||||
over), deploys the result to `~/.config/opencode/opencode.gateway.json`,
|
||||
and exports `LITELLM_KEY` (from `~/.config/mcp-sync/key`) plus
|
||||
`OPENCODE_CONFIG` (pointing at the deployed file — opencode does NOT
|
||||
auto-load extra files in its config dir, so without this export the file
|
||||
is ignored). Silent + non-fatal: a Gitea hiccup never breaks the shell,
|
||||
last-good config stays in place.
|
||||
- `install.sh` — one-time bootstrap (curl-pipe). Fetches `sync.sh`, stores the
|
||||
user's key (0600), and adds the rc hook.
|
||||
|
||||
opencode **merges** config files, so the synced `opencode.gateway.json`
|
||||
coexists with each user's own `~/.config/opencode/opencode.json` and any
|
||||
opencode merges the `OPENCODE_CONFIG` file on top of the user's global
|
||||
`~/.config/opencode/opencode.json` (and under any project `opencode.json`),
|
||||
so the synced config coexists with each user's own settings and any
|
||||
**local-only** MCP servers (e.g. a client-side `kubernetes` pointed at their
|
||||
own kubeconfig). Sync never touches those.
|
||||
|
||||
@@ -37,13 +52,15 @@ curl -fsSL https://git.nic-oconnor.com/public/dotfiles/raw/branch/main/mcp-confi
|
||||
|
||||
Open a new terminal, run `opencode`, and the gateway tools are there.
|
||||
|
||||
## Adding a gateway server (admin)
|
||||
## Adding a gateway server or model (admin)
|
||||
|
||||
1. Register it on the gateway (`charts/inference/values.yaml` →
|
||||
`proxy_config.mcp_servers`), deploy, smoke-test.
|
||||
2. Add a matching entry to `opencode.gateway.json` here and push to `main`.
|
||||
3. Users get it automatically within the sync TTL (or immediately on a new
|
||||
shell). No per-user action.
|
||||
Register it on the gateway (`charts/inference/values.yaml` →
|
||||
`proxy_config.mcp_servers` for MCP servers, `model_list` for models),
|
||||
deploy, smoke-test. That's it — clients auto-discover both: MCP servers
|
||||
through the aggregate `/mcp/` endpoint, models via the `/v1/models` query
|
||||
in `sync.sh`. Users pick them up within the sync TTL (or immediately on a
|
||||
new shell). `opencode.gateway.json` here only needs editing for structural
|
||||
changes (a new provider, endpoint URLs).
|
||||
|
||||
For **per-user / per-target** tools (cluster access, etc.) do NOT add them
|
||||
here — those run client-side with the user's own credentials. See
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"covert-gpt": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "covert-gpt (LiteLLM)",
|
||||
"options": {
|
||||
"baseURL": "https://llm.covert-gpt.com/v1",
|
||||
"apiKey": "{env:LITELLM_KEY}"
|
||||
},
|
||||
"models": {}
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"gitea": {
|
||||
"covert-gpt": {
|
||||
"type": "remote",
|
||||
"url": "https://llm.covert-gpt.com/mcp/gitea",
|
||||
"url": "https://llm.covert-gpt.com/mcp/",
|
||||
"enabled": true,
|
||||
"headers": {
|
||||
"Authorization": "Bearer {env:LITELLM_KEY}"
|
||||
|
||||
+113
-44
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
# mcp-sync — refresh the shared LiteLLM-gateway MCP config on the laptop and
|
||||
# export the user's LiteLLM key into the environment.
|
||||
# mcp-sync — refresh the shared LiteLLM-gateway opencode config on the laptop,
|
||||
# auto-discover the gateway's models, and export the user's LiteLLM key.
|
||||
#
|
||||
# Sourced from the user's shell rc (installed by install.sh) so it runs at the
|
||||
# start of every interactive shell ("session start"). It is intentionally:
|
||||
@@ -10,14 +10,21 @@
|
||||
#
|
||||
# What it does:
|
||||
# 1. Fetch the shared opencode.gateway.json from Gitea (the single source of
|
||||
# truth for which gateway MCP servers exist). The file is identical for
|
||||
# all users — the per-user key is injected via {env:LITELLM_KEY}, never
|
||||
# baked into the file.
|
||||
# 2. Drop it at ~/.config/opencode/opencode.gateway.json. opencode MERGES
|
||||
# config files, so this coexists with the user's own opencode.json and
|
||||
# any local-only MCP servers (e.g. a client-side kubernetes pointed at
|
||||
# their kubeconfig) — those are never touched.
|
||||
# 3. Export LITELLM_KEY from ~/.config/mcp-sync/key so {env:LITELLM_KEY}
|
||||
# truth: the covert-gpt provider + the aggregate MCP endpoint). The file
|
||||
# is identical for all users — the per-user key is injected via
|
||||
# {env:LITELLM_KEY}, never baked into the file.
|
||||
# 2. Auto-discover models: query the gateway's /v1/models with the user's
|
||||
# key and fill the provider's "models" map. New models exposed on the
|
||||
# gateway appear on every box at the next sync — no config edit anywhere.
|
||||
# (opencode cannot list models from a custom provider itself; the map
|
||||
# must be materialized client-side.) MCP servers need no discovery step:
|
||||
# the single /mcp/ endpoint aggregates every server the key can access.
|
||||
# 3. Drop the result at ~/.config/opencode/opencode.gateway.json and export
|
||||
# OPENCODE_CONFIG pointing at it. opencode does NOT auto-load extra files
|
||||
# in its config dir — it only merges known locations, and OPENCODE_CONFIG
|
||||
# is merged between the global opencode.json and any project config. The
|
||||
# user's own opencode.json and local-only MCP servers are never touched.
|
||||
# 4. Export LITELLM_KEY from ~/.config/mcp-sync/key so {env:LITELLM_KEY}
|
||||
# resolves.
|
||||
|
||||
# --- config (overridable via env before sourcing) -------------------------
|
||||
@@ -25,40 +32,14 @@ MCP_SYNC_RAW_URL="${MCP_SYNC_RAW_URL:-https://git.nic-oconnor.com/public/dotfile
|
||||
MCP_SYNC_HOME="${MCP_SYNC_HOME:-$HOME/.config/mcp-sync}"
|
||||
MCP_SYNC_DEST="${MCP_SYNC_DEST:-$HOME/.config/opencode/opencode.gateway.json}"
|
||||
MCP_SYNC_KEYFILE="${MCP_SYNC_KEYFILE:-$MCP_SYNC_HOME/key}"
|
||||
# Throttle: only re-pull if the cached copy is older than this many seconds
|
||||
# (default 1h). Keeps every new shell from hammering Gitea.
|
||||
# Provider whose models are auto-discovered, and the gateway base URL.
|
||||
MCP_SYNC_PROVIDER="${MCP_SYNC_PROVIDER:-covert-gpt}"
|
||||
MCP_SYNC_GATEWAY="${MCP_SYNC_GATEWAY:-https://llm.covert-gpt.com}"
|
||||
# Throttle: only re-sync if the deployed copy is older than this many seconds
|
||||
# (default 1h). Keeps every new shell from hammering Gitea/the gateway.
|
||||
MCP_SYNC_TTL="${MCP_SYNC_TTL:-3600}"
|
||||
|
||||
# --- 1+2. refresh the gateway config (throttled, non-fatal) ---------------
|
||||
_mcp_sync_fetch() {
|
||||
mkdir -p "$(dirname "$MCP_SYNC_DEST")" || return 0
|
||||
|
||||
# Skip if we pulled recently (mtime within TTL). `find -mmin` is portable
|
||||
# enough; fall back to always-fetch if find is unavailable.
|
||||
if [ -f "$MCP_SYNC_DEST" ] && command -v find >/dev/null 2>&1; then
|
||||
if find "$MCP_SYNC_DEST" -mmin -"$(( MCP_SYNC_TTL / 60 ))" 2>/dev/null | grep -q .; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
command -v curl >/dev/null 2>&1 || return 0
|
||||
tmp="$(mktemp 2>/dev/null)" || return 0
|
||||
# -f: fail on HTTP error; -s: silent; -m: hard timeout so a hung network
|
||||
# can't stall the shell.
|
||||
if curl -fsS -m 8 "$MCP_SYNC_RAW_URL" -o "$tmp" 2>/dev/null; then
|
||||
# Only replace if it's non-empty and valid JSON (don't clobber good config
|
||||
# with an error page).
|
||||
if [ -s "$tmp" ] && { ! command -v python3 >/dev/null 2>&1 || python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$tmp" 2>/dev/null; }; then
|
||||
mv "$tmp" "$MCP_SYNC_DEST" 2>/dev/null || rm -f "$tmp"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
else
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- 3. export the per-user key -------------------------------------------
|
||||
# --- export the per-user key (first: model discovery needs it) -------------
|
||||
_mcp_sync_export_key() {
|
||||
[ -f "$MCP_SYNC_KEYFILE" ] || return 0
|
||||
# Strip whitespace/newline; export so {env:LITELLM_KEY} resolves in opencode.
|
||||
@@ -66,6 +47,94 @@ _mcp_sync_export_key() {
|
||||
[ -n "$LITELLM_KEY" ] && export LITELLM_KEY
|
||||
}
|
||||
|
||||
_mcp_sync_fetch
|
||||
# --- refresh config + discover models (throttled, non-fatal) ---------------
|
||||
_mcp_sync_refresh() {
|
||||
mkdir -p "$(dirname "$MCP_SYNC_DEST")" 2>/dev/null || return 0
|
||||
|
||||
# Skip if we synced recently (mtime within TTL). `find -mmin` is portable
|
||||
# enough; fall back to always-sync if find is unavailable.
|
||||
if [ -f "$MCP_SYNC_DEST" ] && command -v find >/dev/null 2>&1; then
|
||||
if find "$MCP_SYNC_DEST" -mmin -"$(( MCP_SYNC_TTL / 60 ))" 2>/dev/null | grep -q .; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
command -v curl >/dev/null 2>&1 || return 0
|
||||
|
||||
# 1. Fetch the shared base config. On failure fall back to the currently
|
||||
# deployed file so model discovery can still refresh it.
|
||||
_base="$(mktemp 2>/dev/null)" || return 0
|
||||
# -f: fail on HTTP error; -s: silent; -m: hard timeout so a hung network
|
||||
# can't stall the shell.
|
||||
if ! curl -fsS -m 8 "$MCP_SYNC_RAW_URL" -o "$_base" 2>/dev/null || [ ! -s "$_base" ]; then
|
||||
rm -f "$_base"; _base=""
|
||||
[ -f "$MCP_SYNC_DEST" ] || return 0
|
||||
fi
|
||||
|
||||
# 2. Discover the gateway's models with the user's key (best-effort).
|
||||
_models=""
|
||||
if [ -n "${LITELLM_KEY:-}" ]; then
|
||||
_models="$(curl -fsS -m 8 -H "Authorization: Bearer $LITELLM_KEY" \
|
||||
"$MCP_SYNC_GATEWAY/v1/models" 2>/dev/null)" || _models=""
|
||||
fi
|
||||
|
||||
# 3. Merge and deploy. python3 validates the base (don't clobber good config
|
||||
# with an error page), injects the discovered models, and — when discovery
|
||||
# failed — carries over the previous deployed models map so a gateway
|
||||
# hiccup never empties the model list. Without python3, deploy the raw
|
||||
# base as-is (no discovery).
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
MCP_SYNC_BASE="$_base" MCP_SYNC_MODELS="$_models" \
|
||||
MCP_SYNC_PY_DEST="$MCP_SYNC_DEST" MCP_SYNC_PY_PROVIDER="$MCP_SYNC_PROVIDER" \
|
||||
python3 - <<'PYEOF' 2>/dev/null || true
|
||||
import json, os, tempfile
|
||||
|
||||
dest = os.environ["MCP_SYNC_PY_DEST"]
|
||||
provider = os.environ["MCP_SYNC_PY_PROVIDER"]
|
||||
|
||||
def load(path):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
|
||||
base_path = os.environ.get("MCP_SYNC_BASE") or dest
|
||||
try:
|
||||
cfg = load(base_path)
|
||||
except Exception:
|
||||
raise SystemExit(0) # invalid base: keep last-good deployed config
|
||||
|
||||
models = {}
|
||||
try:
|
||||
data = json.loads(os.environ.get("MCP_SYNC_MODELS") or "{}")
|
||||
models = {m["id"]: {} for m in data.get("data", []) if m.get("id")}
|
||||
except Exception:
|
||||
pass
|
||||
if not models:
|
||||
try: # discovery failed: carry over the previous deployed map
|
||||
models = load(dest)["provider"][provider]["models"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
prov = cfg.get("provider", {}).get(provider)
|
||||
if prov is not None and models:
|
||||
prov["models"] = models
|
||||
|
||||
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(dest))
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(cfg, f, indent=2)
|
||||
os.replace(tmp, dest)
|
||||
PYEOF
|
||||
elif [ -n "$_base" ]; then
|
||||
mv "$_base" "$MCP_SYNC_DEST" 2>/dev/null || true
|
||||
_base=""
|
||||
fi
|
||||
[ -n "$_base" ] && rm -f "$_base"
|
||||
}
|
||||
|
||||
_mcp_sync_export_key
|
||||
unset -f _mcp_sync_fetch _mcp_sync_export_key 2>/dev/null || true
|
||||
_mcp_sync_refresh
|
||||
|
||||
# opencode only merges config from known locations; point OPENCODE_CONFIG at
|
||||
# the synced file so it is layered on top of the user's global opencode.json.
|
||||
[ -f "$MCP_SYNC_DEST" ] && export OPENCODE_CONFIG="$MCP_SYNC_DEST"
|
||||
|
||||
unset -f _mcp_sync_export_key _mcp_sync_refresh 2>/dev/null || true
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
# overwritten on the next bootstrap run. Edit zsh/zshrc in the dotfiles repo
|
||||
# instead.
|
||||
|
||||
# If this account was added to the docker group (dotfiles/docker/install.sh)
|
||||
# but this shell's session predates that, re-exec via newgrp so docker works
|
||||
# without a full logout. Guarded: fires only when the group is on the account
|
||||
# yet missing from the running shell's groups, so the newgrp'd shell (which
|
||||
# has the group) cannot loop.
|
||||
# Only for real terminals ([ -t 0 ]): newgrp replaces the shell and would
|
||||
# swallow the command in scripted `zsh -i -c ...` invocations.
|
||||
if [ -t 0 ] && [ -S /var/run/docker.sock ] \
|
||||
&& id -nG "${USER:-$(id -un)}" 2>/dev/null | grep -qw docker \
|
||||
&& ! id -Gn 2>/dev/null | grep -qw docker; then
|
||||
exec newgrp docker
|
||||
fi
|
||||
|
||||
# Userland tools installed by dotfiles/bin/install.sh live here.
|
||||
case ":$PATH:" in
|
||||
*":$HOME/.local/bin:"*) ;;
|
||||
@@ -20,3 +33,8 @@ HISTSIZE=10000
|
||||
SAVEHIST=10000
|
||||
setopt SHARE_HISTORY
|
||||
setopt HIST_IGNORE_DUPS
|
||||
|
||||
# Per-machine customizations (secrets, keys, local overrides) live in
|
||||
# ~/.zshrc.local, which bootstrap never touches. See zsh/zshrc.local.example
|
||||
# for the variables modules look for. Loaded last so it can override anything.
|
||||
[ -f "$HOME/.zshrc.local" ] && source "$HOME/.zshrc.local"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# ~/.zshrc.local — per-machine customizations. Copy this file to
|
||||
# ~/.zshrc.local and fill in what this box needs. It is sourced at the end
|
||||
# of the managed ~/.zshrc, is never overwritten by bootstrap, and never
|
||||
# lives in the repo — this is the ONLY place to put secrets.
|
||||
#
|
||||
# cp zsh/zshrc.local.example ~/.zshrc.local && chmod 600 ~/.zshrc.local
|
||||
|
||||
# --- Gitea -------------------------------------------------------------
|
||||
# Personal access token for git.nic-oconnor.com (API/CLI use; HTTPS git
|
||||
# auth itself goes through credential.helper store, see git/).
|
||||
# export GITEA_KEY=...
|
||||
|
||||
# --- covert-gpt LiteLLM gateway -----------------------------------------
|
||||
# LITELLM_KEY is normally exported by mcp-config's sync hook from
|
||||
# ~/.config/mcp-sync/key — run mcp-config/install.sh once instead of
|
||||
# setting it here. Only set it here on a box without mcp-sync:
|
||||
# export LITELLM_KEY=sk-...
|
||||
|
||||
# --- 1Password CLI -------------------------------------------------------
|
||||
# Service-account token for non-interactive `op read` / `op run`.
|
||||
# export OP_SERVICE_ACCOUNT_TOKEN=ops_...
|
||||
|
||||
# --- Anything else -------------------------------------------------------
|
||||
# Aliases, PATH additions, kubeconfig defaults, etc. This file loads last,
|
||||
# so it can override anything the managed zshrc sets.
|
||||
# export KUBECONFIG="$HOME/.kube/homelab.yaml"
|
||||
Reference in New Issue
Block a user