diff --git a/scripts/workstation/.gitignore b/scripts/workstation/.gitignore new file mode 100644 index 00000000..fadfa13d --- /dev/null +++ b/scripts/workstation/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +.pytest_cache/ +*.pyc diff --git a/scripts/workstation/managed-settings.json b/scripts/workstation/managed-settings.json new file mode 100644 index 00000000..aa259ff7 --- /dev/null +++ b/scripts/workstation/managed-settings.json @@ -0,0 +1,3 @@ +{ + "claudeMd": "# Viktor Barzin homelab — shared multi-user Claude Code Workstation (devvm)\n\nYou are running as a specific OS user on a SHARED devvm Workstation, not as the admin. These org-wide rules apply to EVERY user and sit at the top of settings precedence (they cannot be overridden by a user's own config):\n\n- Respect your permission tier. Your kubectl, Vault, and infra access are scoped to your RBAC tier (admin / power-user / namespace-owner). Do not attempt to escalate privileges or reach another user's resources.\n- Secrets are per-user. Never read another user's home directory, credentials, tokens, or ~/.claude secrets. Your own secrets live in your home at mode 600.\n- Infrastructure changes go through Terraform/Terragrunt (scripts/tg apply) — never direct kubectl apply/edit/patch. Pushing to git does NOT deploy; applies are manual and admin-gated, so your edits cannot take effect without an admin apply.\n- Follow the engineering rules in ~/.claude/rules/ (execution, planning, quality) and every CLAUDE.md in the repo tree.\n- The monorepo is at ~/code. Non-admins get a git-crypt-LOCKED clone: secret files read as ciphertext — that is expected, not an error." +} diff --git a/scripts/workstation/setup-devvm.sh b/scripts/workstation/setup-devvm.sh new file mode 100755 index 00000000..2ab909c8 --- /dev/null +++ b/scripts/workstation/setup-devvm.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Idempotent machine-wide host base for the devvm Claude Code Workstation. +# Run as root. Sets up ONLY machine-wide state: the apt toolset, node + claude-code, +# kubelogin, the ENFORCED managed Claude config, and /etc/skel defaults (launcher, +# tmux UX, and live config-inheritance symlinks into the shared config base). +# +# PER-USER provisioning (accounts, per-tier groups, kubeconfig, secrets, infra +# clone) lives in t3-provision-users.sh — NOT here. Safe to re-run. +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# The shared config base every user inherits from (live, chezmoi-versioned). +# Coupled to the admin's home today; override to relocate to a neutral path. +CONFIG_BASE="${WORKSTATION_CONFIG_BASE:-/home/wizard/.claude}" +[[ $EUID -eq 0 ]] || { echo "setup-devvm.sh: must run as root" >&2; exit 1; } +log() { echo "[setup-devvm] $*"; } + +# 1) apt toolset (declarative manifest; comments/blank lines stripped) +mapfile -t PKGS < <(grep -vE '^[[:space:]]*(#|$)' "$HERE/packages.txt") +log "apt: ensuring ${#PKGS[@]} packages present" +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y "${PKGS[@]}" >/dev/null + +# 2) node >= 18 + claude-code (claude-code requires node >= 18) +need_node=1 +if command -v node >/dev/null; then + [[ "$(node -v | sed 's/^v\([0-9]*\).*/\1/')" -ge 18 ]] && need_node=0 +fi +if [[ $need_node -eq 1 ]]; then + log "node: installing NodeSource 22.x" + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - >/dev/null + apt-get install -y nodejs >/dev/null +fi +command -v claude >/dev/null || { log "npm: installing @anthropic-ai/claude-code"; npm install -g @anthropic-ai/claude-code >/dev/null; } + +# 3) kubelogin (kubectl oidc-login) system-wide — NOT the apt 'kubelogin' (= Azure tool) +if [[ ! -x /usr/local/bin/kubelogin ]]; then + log "kubelogin: installing int128/kubelogin" + tmp="$(mktemp -d)" + curl -fsSL -o "$tmp/kl.zip" https://github.com/int128/kubelogin/releases/latest/download/kubelogin_linux_amd64.zip + ( cd "$tmp" && { unzip -o kl.zip kubelogin >/dev/null 2>&1 || python3 -m zipfile -e kl.zip .; } ) + install -m 0755 "$tmp/kubelogin" /usr/local/bin/kubelogin + ln -sf /usr/local/bin/kubelogin /usr/local/bin/kubectl-oidc_login + rm -rf "$tmp" +fi + +# 4) machine-wide ENFORCED Claude config (org claudeMd; top precedence; NO secrets) +install -d -m 0755 /etc/claude-code +install -m 0644 "$HERE/managed-settings.json" /etc/claude-code/managed-settings.json +log "managed-settings.json -> /etc/claude-code/ (enforced org claudeMd)" + +# 5) /etc/skel for NEW accounts: launcher + tmux UX + live-inheritance symlinks. +# A symlink placed in /etc/skel is copied (as a symlink) into each new home by +# `useradd -m`, so new users' ~/.claude/{skills,rules,...} resolve to the shared +# base and pick up the admin's edits live. Secrets + hooks are per-user (written +# by the provisioner), NEVER symlinked here. +install -d -m 0755 /etc/skel +install -m 0755 "$HERE/skel/start-claude.sh" /etc/skel/start-claude.sh +install -m 0644 "$HERE/skel/tmux.conf" /etc/skel/.tmux.conf +install -d -m 0755 /etc/skel/.claude +for d in skills rules agents commands; do + [[ -d "$CONFIG_BASE/$d" ]] && ln -sfn "$CONFIG_BASE/$d" "/etc/skel/.claude/$d" +done +log "skel: launcher + tmux + inheritance symlinks (base=$CONFIG_BASE)" + +log "OK (idempotent)" diff --git a/scripts/workstation/skel/start-claude.sh b/scripts/workstation/skel/start-claude.sh new file mode 100755 index 00000000..7c86cc58 --- /dev/null +++ b/scripts/workstation/skel/start-claude.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Per-user Claude Code Workstation launcher (devvm). Lands the user in their OWN +# ~/code clone (NOT a hardcoded /home/wizard/code) and names the Claude session +# after the tmux session so /resume, the prompt box, and the terminal title line +# up. Deployed via /etc/skel by setup-devvm.sh, so new accounts get it on +# `useradd -m`. Existing users are repointed to this during their migration. +echo "" +echo " Welcome, $(id -un)! 🚀" +echo "" +echo " Starting Claude Code in $HOME/code ..." +echo " (Right-click for tmux menu, or Ctrl+B then | or - to split)" +echo "" + +name_args=() +if [ -n "${TMUX:-}" ]; then + sess="$(tmux display-message -p '#{session_name}' 2>/dev/null)" + [ -n "$sess" ] && name_args=(--name "$sess") +fi + +cd "$HOME/code" 2>/dev/null || cd "$HOME" + +# Prefer the system-wide `claude` (installed by setup-devvm.sh); fall back to npx. +launch() { + if command -v claude >/dev/null 2>&1; then + claude "$@" + else + npx @anthropic-ai/claude-code "$@" + fi +} + +# Deliberately not `exec` so we can branch on the exit code: clean quit ends the +# pane (ttyd closes the terminal); a crash drops to a shell so the tmux session +# isn't destroyed-and-recreated in a ttyd auto-reconnect loop. +launch --dangerously-skip-permissions --model claude-opus-4-8 "${name_args[@]}" +code=$? +[ "$code" -eq 0 ] && exit 0 + +echo "" +echo " claude exited abnormally (status $code). Dropping to a shell — your tmux session is preserved." +echo " Re-launch any time with: ~/start-claude.sh" +echo "" +exec "${SHELL:-/bin/bash}" -l diff --git a/scripts/workstation/skel/tmux.conf b/scripts/workstation/skel/tmux.conf new file mode 100644 index 00000000..0324834e --- /dev/null +++ b/scripts/workstation/skel/tmux.conf @@ -0,0 +1,51 @@ +# Workstation base tmux config (deployed to /etc/skel/.tmux.conf by +# setup-devvm.sh; new accounts inherit it). Uses $HOME (expanded by the shell at +# run time) so it works for ANY user — never a hardcoded /home/. +# +# NOTE: the tmux-resurrect/continuum "persistence" block is owned by the separate +# terminal-lobby tool, which appends its own managed section + installs tpm. This +# base file intentionally omits it so a fresh account isn't left with broken +# `run ~/.tmux/plugins/tpm/tpm` references before terminal-lobby runs. + +# Launch the per-user Claude launcher in every new pane/window (lands in ~/code). +set -g default-command "$HOME/start-claude.sh" + +# Mouse support — click panes, drag to resize, scroll with wheel +set -g mouse on + +# Easy splits: Ctrl+b then | for vertical, - for horizontal +bind | split-window -h -c "#{pane_current_path}" +bind - split-window -v -c "#{pane_current_path}" +bind c new-window -c "#{pane_current_path}" + +# Right-click context menu — clickable actions popup +bind -n MouseDown3Pane display-menu -T "#[align=centre]Terminal Menu" -x M -y M \ + "New Claude" w "new-window -c '#{pane_current_path}'" \ + "Split Horizontal" h "split-window -v -c '#{pane_current_path}'" \ + "Split Vertical" v "split-window -h -c '#{pane_current_path}'" \ + "" \ + "Shell" s "split-window -v -c '#{pane_current_path}' /bin/zsh" \ + "" \ + "Close Pane" x "confirm-before -p 'Close pane? (y/n)' kill-pane" \ + "Close Window" X "confirm-before -p 'Close window? (y/n)' kill-window" \ + "" \ + "Detach" d "detach-client" + +# Clickable [+] button in the status bar — left-click to open the same menu +set -g status-right '#[fg=black bg=green] [+] #[default] #[fg=cyan]Right-click for menu ' +set -g status-right-length 60 +bind -n MouseDown1StatusRight display-menu -T "#[align=centre]Terminal Menu" -x M -y S \ + "New Claude" w "new-window -c '#{pane_current_path}'" \ + "Split Horizontal" h "split-window -v -c '#{pane_current_path}'" \ + "Split Vertical" v "split-window -h -c '#{pane_current_path}'" \ + "" \ + "Shell" s "split-window -v -c '#{pane_current_path}' /bin/zsh" \ + "" \ + "Close Pane" x "confirm-before -p 'Close pane? (y/n)' kill-pane" \ + "Close Window" X "confirm-before -p 'Close window? (y/n)' kill-window" + +# Status bar styling + 1-based numbering +set -g status-style 'bg=colour235 fg=colour136' +set -g status-left '#[fg=green][#S] ' +set -g base-index 1 +setw -g pane-base-index 1