Commit Graph
22 Commits
Author SHA1 Message Date
noconnorandClaude Opus 5 86b8e51646 Remove the docker module: dotfiles no longer installs Docker
Installing Docker Engine is a host/image decision, not a per-user
environment one. In Coder workspaces (unprivileged pods, no systemd) the
module ran get.docker.com on every start for a daemon that can never
run there. Where Docker is wanted, install it on the host or bake it
into the image.

Also drops the zshrc newgrp-docker hook that existed only for this
module's group change, and the docs/skills references to docker/.
docker-rootless-cgroup/ (configures Kasm's existing rootless daemon)
and bin/'s kind gate (detects a daemon) stay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 13:39:59 -04:00
noconnorandClaude Opus 5 5bc8a27f60 secrets: fail loudly on a bad 1Password token instead of a LiteLLM prompt
A bad token paste (or a vault/item that did not match the login-name
default) used to surface only as mcp-config asking for a LiteLLM key,
hiding that every vault key was missing. Now:

- the token is verified with `op whoami` and re-prompted on a bad paste
- vault and item are validated, auto-picked when the token sees only one,
  and otherwise prompted with the list it can see
- bootstrap skips mcp-config and hermes, with an error naming the cause,
  when no LITELLM_KEY came from the vault

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 11:55:27 -04:00
noconnorandClaude Opus 5 8ec3269f4a bin: install kubelogin as the kubectl oidc-login plugin
omnictl-generated kubeconfigs authenticate via `kubectl oidc-login
get-token`, so install int128/kubelogin as kubectl-oidc_login.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 11:35:15 -04:00
noconnorandClaude Opus 5 1159197e4b secrets: rename the vault helpers from env* to op*
They act on 1Password, not on the environment, so the prefix now says so:
opr (read), opc (create/update), opd (delete), opl (list), ops (refresh).
No behavior change. Checked the new names don't collide with any command
or oh-my-zsh alias, and verified opr/opl/ops against the real vault
(values compared, not printed).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 16:36:20 +00:00
noconnorandClaude Opus 5 c6e5f22acb secrets: read the 1Password vault once into a 0600 env file
Secrets were pulled in each box's hand-written ~/.zshrc.local with one
`op read` per field on EVERY new shell: every terminal waited on
1Password, every shell broke when it was unreachable, and the loop lived
outside the repo.

New secrets/ module (after bin/, which provides op and jq): one vault per
user and one item, both defaulting to the login name, whose field labels
are the variable names. It asks for the service-account token once
(hidden), fetches the item with a single `op item get`, and writes every
valid-named field to ~/.config/dotfiles/secrets.env (0600), replaced
atomically so a failed fetch keeps the previous copy. Values go through
jq @sh, so quotes, $() and newlines in a secret can't break or inject.
The token is kept in op.env (0600), which shells never source.

bootstrap loads secrets.env right after the module, so mcp-config takes
LITELLM_KEY from the env instead of prompting. A LITELLM_KEY rotated in
the vault also updates ~/.config/mcp-sync/key, since sync.sh re-exports
from that file after secrets.env loads and would otherwise win.

The managed zshrc sources secrets.env (no op call at startup) and adds
envr / envc / envd / envl / envs to read, upsert, delete, list and
refresh; they load the token only in a subshell.

Existing boxes still need the old op loop removed from ~/.zshrc.local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:30:24 +00:00
noconnorandClaude Opus 5 1acbd28c6e bin: add the coder CLI
Pinned to v2.36.5 (latest release). Same shape as the other tarball
installs: the asset drops the tag's leading v and the binary sits at the
archive root. Verified on this box: installs, `coder version` reports
v2.36.5, and a re-run skips it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:30:24 +00:00
noconnorandClaude Opus 5 135112eee5 mcp-config/hermes: make the gateway's context limits actually reach the clients
Folds in the in-flight local-checkout work and fixes the context accounting
on top of it. Both halves are the same bug seen from two ends: clients were
sending requests larger than the backend's window and getting hard
ContextWindowExceededError 400s, intermittently, in a way that looked like
the model or the GPU misbehaving.

Already in flight here (unchanged in intent):
  - sync.sh prefers the LOCAL CHECKOUT over re-downloading from Gitea, and
    install.sh records the checkout path so it can be found. That recording
    is load-bearing: without it sync.sh cannot find the repo, silently falls
    back to Gitea, and every local edit to opencode.gateway.json is ignored.
    Observed on this box — ~/.config/mcp-sync/source did not exist, the
    default ~/.dotfiles is not where the checkout lives, and the Gitea base
    still served models:{}.
  - the hermes custom-provider wiring.

Fixed on top:

opencode had NO context limit on ANY gateway model, not a wrong one. Two
faults compounded. /model/info 403s for a day-to-day virtual key (only
management-scoped keys may read it), which sync.sh treats as best-effort and
falls back to names-only — as designed and as commented. But the merge then
did `prov["models"] = models`, replacing the map wholesale, so limits
declared in the base config were discarded too and there was no floor left.
Base specs are now a floor that live /model/info overrides field by field:
discovery still wins where present, so this adds no drift, and a 403 no
longer strips the limits. Verified: ornith 195584/32768, the qwens
261120/32768.

hermes' model map named `qwen3-vl-30b-a3b` at 262144 — a model the gateway
stopped serving when Ornith replaced it — while ornith-1.0-35b, the model
actually on cg-1 AND the only one with a window smaller than its neighbours
(196608), had no entry at all and inherited Hermes' default. The one model
needing a lower ceiling was the one not declaring one, which is exactly the
shape that fails only once a session grows.

NOTE for other boxes: hermes reads ~/.hermes/config.yaml, which install.sh
produces by MERGING gateway.yaml. Editing gateway.yaml alone changes nothing
until install.sh re-runs — this machine's live config was patched by hand.

Not addressed here, deliberately: the gateway-side context_window_fallback
(ai-k8s-iac). It is wired but inert while cg-2 is scaled down, so preventing
the oversized request client-side is what actually works today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FoDK4wwB1Twty5gwiriYLf
2026-09-11 16:38:03 -04:00
noconnorandClaude Opus 4.8 11f88ad3a9 mcp-config: render the SAME MCP servers into Hermes' config
Hermes had no MCP servers at all -- no mcp_servers: key -- so "use the gitea
MCP" had nothing to call. The model said so ("there's no Gitea-specific skill"),
which was true, and then looped reaching for the browser as the only way in.

The obvious fix -- paste a gitea block into ~/.hermes/config.yaml -- is the
wrong one: it makes a SECOND definition of a server this repo already defines
once, free to drift (one pinned to gitea-mcp@0.0.10, the other to whatever got
typed). README already calls this out: "all MCP servers are defined here", and
listed other clients as "not done yet". So render, don't duplicate: sync.sh
already translates LiteLLM's /model/info into opencode's schema; this is the
same move into Hermes' schema, from opencode.gateway.json's mcp block. Add a
server once, both clients get it within the same TTL.

Not a shared server PROCESS, which is what "use the same one" would imply: MCP
stdio servers are subprocesses spawned per client over stdin/stdout and dying
with it. Each client running its own `npx -y <pkg>` is the design -- same
package, same pinned version, same per-user credential, same identity. Sharing
one process would need a persistent daemon plus a stdio->HTTP bridge, for
nothing. And it stays client-side: the gateway hosts no MCP servers on purpose
(values.yaml:892; user-scoped APIs, no shared tokens), and gitea-mcp hard-exits
without a token so LiteLLM's headerless listing never registers it anyway.

Translation notes: Hermes' `command` must be a STRING (a list stringifies to
"['npx', '-y']" and fails at exec), so opencode's argv list splits into
command + args. `{env:VAR}` becomes `${VAR}` -- both by-name, so no secret is
written to either file. Hermes gives an MCP child only a filtered baseline env,
so every secret must be listed in env:, which the rendered block does. Nothing
is added to platform_toolsets: MCP servers auto-enable by name, and listing some
would turn it into an allowlist.

Rendered names are recorded in ~/.config/mcp-sync/hermes-managed.json so a
server dropped upstream is dropped from Hermes, while servers the user added by
hand -- never in the manifest -- are left alone. Only writes when the result
changes, skips silently when Hermes isn't installed, warns when PyYAML is
missing (an absent MCP block is invisible rather than obviously broken).

Verified via Hermes' own discovery: 72 tools register -- 30 gitea (incl.
issue__list) and 42 proxmox -- with ${GITEA_KEY} / ${PROXMOX_MCP_*} resolving
and no secret in config.yaml.

Also fixes a stale README claim that MCP tools come from the gateway's /mcp
endpoint; they never do, and values.yaml:892 says why.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:04:57 -04:00
noconnorandClaude Opus 4.8 e42807c054 hermes: wire into dotfiles; fix opencode's generated context limits
Two client-side fixes against the covert-gpt LiteLLM gateway, both traced
from opencode throwing intermittent ContextWindowExceededError 400s.

opencode (mcp-config/sync.sh):
  spec_for() mapped LiteLLM's /model/info onto opencode's schema 1:1, but the
  two mean different things by "output". LiteLLM's max_output_tokens is the
  whole window (vLLM caps TOTAL context, not output separately, which is
  correct for LiteLLM); opencode's limit.output is a RESERVE it subtracts from
  context to pick a compaction point. Copying one to the other told opencode to
  reserve the entire 262144 window, so it targeted an input of context-output
  and the server counted one token more than opencode did -- landing on
  context+1 and a hard 400, every time (measured: 196609 in + 65536 out =
  262145 vs a 262144 window; the retries shrinking max_tokens by 65 never
  helped because the input grew to compensate).

  Capping the reserve alone would NOT have fixed it -- opencode would still sit
  exactly on the boundary, just at a different input size. So: cap the reserve
  (OUTPUT_RESERVE_CAP) and hold a margin back from context
  (CONTEXT_SAFETY_MARGIN). Generated at sync time, so it fixes every gateway
  model at once, including third-party ones.

hermes (hermes/gateway.yaml, hermes/install.sh):
  Hermes reached the gateway by pretending it WAS OpenRouter -- the LiteLLM key
  in OPENROUTER_API_KEY, the gateway in OPENROUTER_BASE_URL. Requests routed
  fine, but fetch_openrouter_models hardcodes openrouter.ai/api/v1/models and
  ignores OPENROUTER_BASE_URL, so the picker listed OpenRouter's ~342-model
  catalog -- models this gateway cannot serve -- while the gateway's own models,
  including cg-1's, never appeared at all.

  Replaced with a named custom provider. install.sh now merges gateway.yaml into
  ~/.hermes/config.yaml, so a fresh box gets a Hermes that can actually talk to
  the gateway instead of one pointed at nothing. The merge is idempotent and
  narrow: only the covert-gpt entry is touched, model.default/provider are
  seeded only when unset, and a box still on the masquerade is migrated off it
  (fingerprint: provider openrouter + base_url pointing at this gateway; a
  genuine OpenRouter user never has that, and is left alone).

  No secrets: gateway.yaml names the key via key_env, and mcp-config/sync.sh is
  what exports it. Hermes needs no context safety margin -- it omits max_tokens
  entirely, so vLLM bounds output by whatever context is left.

Also folds in the in-flight hermes bootstrap wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 10:38:12 -04:00
noconnorandClaude Opus 4.8 fdc3a71236 docker: install from the docker-ce repo on RHEL-family hosts
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>
2026-07-16 06:59:18 -04:00
noconnor 836eea6fc6 bin/install.sh: add packer installation 2026-07-13 16:08:53 -04:00
noconnorandClaude Opus 4.8 6dc34c5dbb bin/: add zarf
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>
2026-07-13 13:10:11 -04:00
noconnorandClaude Opus 4.8 70b049625a mcp-config: run MCP servers client-side, not on the gateway
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>
2026-07-13 13:10:11 -04:00
noconnorandClaude Opus 4.8 c0df3a1596 README: document docker/, new tools, and ~/.zshrc.local
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>
2026-07-12 04:40:25 -04:00
noconnorandClaude Opus 4.8 eff01b1b0b Add docker-rootless-cgroup module for cgroup v2 delegation
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>
2026-07-08 11:53:46 -04:00
noconnorandClaude Opus 4.8 39f638b8db bin/: add kind, gated on rootless Docker availability
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>
2026-07-08 10:53:46 -04:00
noconnorandClaude Opus 4.8 ebdc213444 bin/: swap seed tools for devops CLIs; add ansible/ module
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>
2026-07-08 10:34:01 -04:00
noconnorandClaude Opus 4.8 fd88d8bba2 Add bin/ module: userland CLI tools into ~/.local/bin
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>
2026-07-08 10:25:18 -04:00
noconnorandClaude Opus 4.8 e1515335e8 Self-clone in bootstrap.sh so curl-pipe works; document quick start
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>
2026-07-08 10:15:39 -04:00
noconnorandClaude Sonnet 5 1be0a37c26 Add missing trailing newline to README
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 09:39:03 -04:00
noconnorandClaude Sonnet 5 d4e3121d68 Add bootstrap.sh with zsh and git modules
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>
2026-07-08 09:36:52 -04:00
noconnor 04db500c85 Initial commit 2026-06-15 20:53:12 -04:00