#!/usr/bin/env bash # dev-add.sh - Add or update a dev user account # Usage: dev-add.sh # # Idempotent: safe to re-run. Updates SSH key and config if user exists. # # Creates/updates: # - Unix user account with SSH key # - Adds to devs group (Slack token access) # - Git config (user.name, user.email) # - Forgejo account for git.clarun.xyz access # - Outputs onboarding instructions set -euo pipefail # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } usage() { echo "Usage: $0 " echo "" echo "Arguments:" echo " username - Dev's username (alphanumeric, 3-16 chars)" echo " ssh-pubkey - SSH public key (starts with ssh-ed25519, ssh-rsa, etc.)" echo "" echo "Example:" echo " $0 alice 'ssh-ed25519 AAAA... alice@laptop'" exit 1 } validate_username() { local username="$1" if [[ ! "$username" =~ ^[a-z][a-z0-9_-]{2,15}$ ]]; then log_error "Invalid username: must be 3-16 chars, start with letter, alphanumeric/underscore/dash only" exit 1 fi } validate_ssh_key() { local key="$1" if [[ ! "$key" =~ ^ssh-(ed25519|rsa|ecdsa) ]]; then log_error "Invalid SSH key: must start with ssh-ed25519, ssh-rsa, or ssh-ecdsa" exit 1 fi } create_user() { local username="$1" local ssh_key="$2" local user_exists=false # Check if user already exists if id "$username" &>/dev/null; then user_exists=true log_info "Updating existing user '$username'..." else log_info "Creating user '$username'..." # Create user with home directory # NixOS: don't specify shell (uses default), group is 'users' useradd -m -g users "$username" fi # Make home directory private (not world-readable) chmod 700 "/home/$username" # Add to devs group for Slack token access if ! groups "$username" | grep -q '\bdevs\b'; then usermod -aG devs "$username" log_info "Added to devs group" fi # Set up SSH directory and login key (authorized_keys) local ssh_dir="/home/$username/.ssh" mkdir -p "$ssh_dir" echo "$ssh_key" > "$ssh_dir/authorized_keys" chmod 700 "$ssh_dir" chmod 600 "$ssh_dir/authorized_keys" chown -R "$username:users" "$ssh_dir" if [[ "$user_exists" == true ]]; then log_info "Updated SSH login key" fi # Generate server-side keypair for git access (if not exists) local server_key="$ssh_dir/id_ed25519" if [[ ! -f "$server_key" ]]; then log_info "Generating server-side SSH key for git access..." sudo -u "$username" ssh-keygen -t ed25519 \ -f "$server_key" \ -N '' \ -C "$username@jrz1-server-DO-NOT-REUSE" \ >/dev/null log_info "Server-side SSH key generated" else log_info "Server-side SSH key already exists" fi # Ensure strict permissions chmod 700 "$ssh_dir" chmod 600 "$server_key" chmod 644 "$server_key.pub" chown -R "$username:users" "$ssh_dir" # Set up user's shell config (may not exist on NixOS) # .profile = login shells (SSH), .bashrc = interactive non-login local profile="/home/$username/.profile" { echo '# bun global packages (preferred - faster installs)' echo "export PATH=\"\$HOME/.bun/bin:\$PATH\"" echo '' echo '# npm global packages (fallback)' echo "export PATH=\"\$HOME/.npm-global/bin:\$PATH\"" echo '' echo '# Slack bot development tokens (if readable)' echo '[ -r /etc/slack-dev.env ] && source /etc/slack-dev.env' } > "$profile" chown "$username:users" "$profile" # Pre-create npm global directory and configure npm prefix local npm_global="/home/$username/.npm-global" local npmrc="/home/$username/.npmrc" mkdir -p "$npm_global" echo "prefix=$npm_global" > "$npmrc" chown "$username:users" "$npm_global" "$npmrc" # Create AGENTS.md for AI coding assistants cat > "/home/$username/AGENTS.md" << 'AGENTS_EOF' # AGENTS.md - Dev Server Guide Shared NixOS dev server. This guide helps AI coding agents work effectively here. ## Possible Right Now **Install packages:** ```bash bun install -g @google/gemini-cli # JS tools (fast) nix profile install nixpkgs#go # System tools (go, rust, etc.) uv venv && uv pip install # Python packages ``` **Run services:** ```bash python -m http.server 8080 # Dev servers on high ports bun run app.js # Run JS directly tmux / screen # Persistent sessions ``` **Available tools:** python3, uv, bun, node, npm, zig, git, vim, curl, tmux, opencode, bd ## Not Possible Right Now | Want | Why | Workaround | |------|-----|------------| | sudo / root | Shared server security | Use nix profile or npm install -g | | apt / yum | NixOS uses nix | `nix profile install nixpkgs#` | | Port 80/443 | Needs root | Use high port + SSH tunnel | | Docker | Security isolation | Use nix for dependencies | | systemd system services | Needs root | Use pm2, screen, or tmux | ## Resource Limits Per-user limits to keep the server stable: - **Memory**: ~1GB (50% of system) - **Processes**: 200 max - **Network**: 30 new connections/min Heavy processes may be killed automatically. ## Environment - **OS**: NixOS (not Debian/Ubuntu) - **Shell**: bash - **Home**: ~/ (private, 700) - **Temp**: /tmp (fast, cleared on reboot) ## AI Agent Sandbox Conflicts Some AI agents (Codex, etc.) sandbox commands with seccomp filters, blocking nix daemon access. **Symptom**: `nix store ping` fails with "Operation not permitted" inside the agent but works in your shell. **Fix for Codex CLI**: ```bash # One-off codex -s danger-full-access # Permanent (~/.codex/config.toml) sandbox_mode = "danger-full-access" ``` Server already provides isolation - agent sandbox is redundant here. AGENTS_EOF chown "$username:users" "/home/$username/AGENTS.md" # Set up git config for proper commit attribution local gitconfig="/home/$username/.gitconfig" cat > "$gitconfig" << EOF [user] name = $username email = $username@clarun.xyz [init] defaultBranch = main EOF chown "$username:users" "$gitconfig" log_info "Git config created" # Provision Forgejo account for git.clarun.xyz access provision_forgejo "$username" "$ssh_key" if [[ "$user_exists" == true ]]; then log_info "User updated" else log_info "User created with SSH access" fi } # Helper function to upload a single SSH key to Forgejo upload_forgejo_key() { local username="$1" local pubkey="$2" local title="$3" local token="$4" local forgejo_url="http://localhost:3000" local http_code http_code=$(curl -s -o /dev/null -w "%{http_code}" \ --connect-timeout 5 --max-time 30 \ -X POST "$forgejo_url/api/v1/admin/users/$username/keys" \ -H "Authorization: token $token" \ -H "Content-Type: application/json" \ -d "{ \"title\": \"$title\", \"key\": \"$pubkey\", \"read_only\": false }" 2>/dev/null) || true if [[ "$http_code" == "201" ]]; then log_info "Forgejo key '$title' added" elif [[ "$http_code" == "422" ]]; then log_info "Forgejo key '$title' already exists" else log_warn "Forgejo key '$title' upload returned HTTP $http_code" fi } provision_forgejo() { local username="$1" local login_key="$2" local token_file="/run/secrets/forgejo-api-token" local forgejo_url="http://localhost:3000" # Check if token file exists if [[ ! -r "$token_file" ]]; then log_warn "Forgejo API token not found at $token_file" log_warn "Skipping Forgejo provisioning - user can self-register at https://git.clarun.xyz" return 0 fi local token token=$(cat "$token_file") # Try to create Forgejo user (ignore if already exists) local random_pass random_pass=$(head -c 16 /dev/urandom | base64 | tr -d '/+=' | head -c 16) local http_code http_code=$(curl -s -o /dev/null -w "%{http_code}" \ --connect-timeout 5 --max-time 30 \ -X POST "$forgejo_url/api/v1/admin/users" \ -H "Authorization: token $token" \ -H "Content-Type: application/json" \ -d "{ \"username\": \"$username\", \"email\": \"$username@clarun.xyz\", \"password\": \"$random_pass\", \"must_change_password\": true, \"send_notify\": false }" 2>/dev/null) || true local user_created=false if [[ "$http_code" == "201" ]]; then log_info "Forgejo user created" user_created=true elif [[ "$http_code" == "422" ]]; then log_info "Forgejo user already exists" else log_warn "Forgejo user creation returned HTTP $http_code (may already exist)" fi # Write credentials file (only if we created the user and know the password) if [[ "$user_created" == true ]]; then local creds_file="/home/$username/.forgejo-credentials" cat > "$creds_file" << EOF { "service": "forgejo", "url": "https://git.clarun.xyz", "username": "$username", "initial_password": "$random_pass", "must_change_password": true, "note": "Delete this file after first login: rm ~/.forgejo-credentials" } EOF chmod 600 "$creds_file" chown "$username:users" "$creds_file" log_info "Credentials written to ~/.forgejo-credentials" fi # Upload BOTH keys to Forgejo: # 1. Login key (user's laptop key) - for direct laptop→git access upload_forgejo_key "$username" "$login_key" "$username-laptop" "$token" # 2. Server-side key - for git access from within the server local server_pubkey_file="/home/$username/.ssh/id_ed25519.pub" if [[ -r "$server_pubkey_file" ]]; then local server_pubkey server_pubkey=$(cat "$server_pubkey_file") upload_forgejo_key "$username" "$server_pubkey" "$username-devserver" "$token" else log_warn "Server-side key not found at $server_pubkey_file" fi } print_onboarding() { local username="$1" local server_ip # NixOS: use ip command instead of hostname -I server_ip=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1) echo "" echo "==========================================" echo " Dev Environment Ready: $username" echo "==========================================" echo "" echo "## SSH Config (~/.ssh/config on your laptop)" echo "" echo " Host dev-server" echo " HostName ${server_ip:-}" echo " User $username" echo " LocalForward 8080 127.0.0.1:8080" echo "" echo "## Quick Start" echo "" echo "1. SSH in:" echo " ssh dev-server" echo "" echo "2. Install AI tools (pick one or more):" echo " bun install -g @anthropic-ai/claude-code # Claude" echo " bun install -g @google/gemini-cli # Gemini" echo "" echo "3. Authenticate and start coding:" echo " claude # or: gemini" echo " # Follow prompts to authenticate" echo "" echo "## Git Access" echo "" echo " Clone repos: git clone forgejo@git.clarun.xyz:org/repo.git" echo "" echo " Forgejo account: $username" echo " Credentials file: ~/.forgejo-credentials" echo " Web UI: https://git.clarun.xyz" echo "" echo " Note: Change password on first login, then delete credentials file" echo "" echo "## Tools Available" echo " System: python3, uv, git, bun, node, opencode, bd" echo " Install more: bun install -g " echo " nix profile install nixpkgs#" echo "" echo "==========================================" } main() { if [[ $# -lt 2 ]]; then usage fi local username="$1" local ssh_key="$2" # Must run as root if [[ $EUID -ne 0 ]]; then log_error "This script must be run as root" exit 1 fi validate_username "$username" validate_ssh_key "$ssh_key" # Check devs group exists (created by NixOS config) if ! getent group devs >/dev/null; then log_error "Required group 'devs' does not exist" log_error "Ensure users.groups.devs = {} is in NixOS config and deployed" exit 1 fi create_user "$username" "$ssh_key" print_onboarding "$username" log_info "Dev user '$username' setup complete!" } main "$@"