diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9dcf3a..046430e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,20 +8,6 @@ on: env: CARGO_TERM_COLOR: always - SYSTEM_DEPS: >- - build-essential - pkg-config - libglib2.0-dev - libgtk-4-dev - libadwaita-1-dev - libcairo2-dev - libpango1.0-dev - libgdk-pixbuf-xlib-2.0-dev - libwayland-dev - libx11-dev - libxkbcommon-dev - libgirepository1.0-dev - libgtk4-layer-shell-dev jobs: build: @@ -30,17 +16,18 @@ jobs: container: image: ubuntu:25.10 steps: - - name: Install git and dependencies + - name: Install git run: | apt-get update - apt-get install -y git curl ${{ env.SYSTEM_DEPS }} + apt-get install -y git curl - uses: actions/checkout@v4 - - name: Setup Rust toolchain - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - echo "$HOME/.cargo/bin" >> $GITHUB_PATH + - name: Install build dependencies + run: ./scripts/shepherd deps install build + + - name: Add Rust to PATH + run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: Cache cargo registry and build uses: actions/cache@v4 @@ -56,7 +43,7 @@ jobs: - name: Build run: | . "$HOME/.cargo/env" - cargo build --all-targets + ./scripts/shepherd build test: name: Test @@ -64,17 +51,18 @@ jobs: container: image: ubuntu:25.10 steps: - - name: Install git and dependencies + - name: Install git run: | apt-get update - apt-get install -y git curl ${{ env.SYSTEM_DEPS }} + apt-get install -y git curl - uses: actions/checkout@v4 - - name: Setup Rust toolchain - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - echo "$HOME/.cargo/bin" >> $GITHUB_PATH + - name: Install build dependencies + run: ./scripts/shepherd deps install build + + - name: Add Rust to PATH + run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: Cache cargo registry and build uses: actions/cache@v4 @@ -98,19 +86,23 @@ jobs: container: image: ubuntu:25.10 steps: - - name: Install git and dependencies + - name: Install git run: | apt-get update - apt-get install -y git curl ${{ env.SYSTEM_DEPS }} + apt-get install -y git curl - uses: actions/checkout@v4 - - name: Setup Rust toolchain + - name: Install build dependencies + run: ./scripts/shepherd deps install build + + - name: Add clippy component run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y . "$HOME/.cargo/env" rustup component add clippy - echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Add Rust to PATH + run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: Cache cargo registry and build uses: actions/cache@v4 @@ -127,3 +119,21 @@ jobs: run: | . "$HOME/.cargo/env" cargo clippy --all-targets -- -D warnings + + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install ShellCheck + run: | + sudo apt-get update + sudo apt-get install -y shellcheck + + - name: Run ShellCheck + run: | + # SC1091: Not following sourced files (info only, safe to ignore) + shellcheck -e SC1091 scripts/shepherd scripts/dev scripts/admin + shellcheck -e SC1091 scripts/lib/*.sh + shellcheck -e SC1091 run-dev diff --git a/run-dev b/run-dev index 7e562aa..17a3b9c 100755 --- a/run-dev +++ b/run-dev @@ -1,59 +1,5 @@ #!/usr/bin/env bash +# Development run script - thin wrapper for shepherd dev run +# This script is kept for backwards compatibility with existing workflows (i.e. lazy devs) -set -e - -# Set up dev runtime directory -DEV_RUNTIME="./dev-runtime" -DATA_DIR="$DEV_RUNTIME/data" -SOCKET_PATH="$DEV_RUNTIME/shepherd.sock" - -mkdir -p "$DATA_DIR" - -# Kill any existing shepherd dev instances before starting -echo "Cleaning up any existing dev instances..." -pkill -f "sway -c ./sway.conf" 2>/dev/null || true -pkill -f "shepherdd" 2>/dev/null || true -pkill -f "shepherd-launcher" 2>/dev/null || true -pkill -f "shepherd-hud" 2>/dev/null || true -# Remove stale socket -rm -f "$SOCKET_PATH" -sleep 0.5 - -# Export environment variables for shepherd binaries -export SHEPHERD_SOCKET="$SOCKET_PATH" -export SHEPHERD_DATA_DIR="$DATA_DIR" - -# Note: Since shepherdd now runs inside sway, spawned apps automatically -# use the nested compositor's display. No SHEPHERD_WAYLAND_DISPLAY override needed. - -# Build all binaries -echo "Building shepherd binaries..." -cargo build - -# Function to cleanup background processes on exit -cleanup() { - echo "Cleaning up..." - # Kill the nested sway - this will clean up everything inside it (including shepherdd) - if [ ! -z "$SWAY_PID" ]; then - kill $SWAY_PID 2>/dev/null || true - fi - # Also explicitly kill any shepherd processes that might have escaped - pkill -f "shepherdd" 2>/dev/null || true - pkill -f "shepherd-launcher" 2>/dev/null || true - pkill -f "shepherd-hud" 2>/dev/null || true - # Remove socket - rm -f "$SOCKET_PATH" -} -trap cleanup EXIT - -# Note: shepherdd is started by sway.conf so it runs INSIDE the nested compositor. -# This ensures all spawned processes (games, apps) use the nested compositor's display. - -# Start sway with the launcher and HUD -# The HUD and launcher are started by sway.conf so they run INSIDE the nested compositor -echo "Starting nested sway with shepherd-launcher..." -WLR_BACKENDS=wayland WLR_LIBINPUT_NO_DEVICES=1 sway -c ./sway.conf & -SWAY_PID=$! - -# Wait for sway to exit -wait $SWAY_PID +exec ./scripts/shepherd dev run "$@" diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..4d7d009 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,148 @@ +# Shepherd Scripts System + +This directory contains the unified script system for shepherd-launcher. + +## Quick Reference + +```sh +# Main entry point +./shepherd --help + +# Dependencies +./shepherd deps print build|run|dev +./shepherd deps install build|run|dev + +# Building +./shepherd build [--release] + +# Development +./shepherd dev run + +# Installation +./shepherd install all --user USER [--prefix PREFIX] +./shepherd install bins [--prefix PREFIX] +./shepherd install config --user USER + +# Hardening +./shepherd harden apply --user USER +./shepherd harden revert --user USER +``` + +## Structure + +``` +scripts/ +├── shepherd # Main CLI dispatcher +├── dev # Wrapper → shepherd dev run +├── admin # Wrapper → shepherd install/harden +├── lib/ # Shared libraries +│ ├── common.sh # Logging, error handling, sudo helpers +│ ├── deps.sh # Dependency management +│ ├── build.sh # Cargo build logic +│ ├── sway.sh # Nested sway execution +│ ├── install.sh # Installation logic +│ └── harden.sh # User hardening/unhardening +└── deps/ # Package lists + ├── build.pkgs # Build-time dependencies + ├── run.pkgs # Runtime dependencies + └── dev.pkgs # Development extras +``` + +## Design Principles + +1. **Single source of truth**: All dependency lists are defined once in `deps/*.pkgs` +2. **Composable**: Each command can be called independently +3. **Reversible**: All destructive actions (hardening, installation) can be undone +4. **Shared logic**: Business logic lives in libraries, not duplicated across scripts +5. **Clear separation**: Build-only, runtime-only, and development dependencies are separate + +## Usage Examples + +### For Developers + +```sh +# First time setup (installs system packages + Rust via rustup) +./shepherd deps install dev +./shepherd dev run + +# Or use the convenience wrapper +./run-dev +``` + +### For CI + +```sh +# Install only build dependencies (includes Rust via rustup) +./shepherd deps install build + +# Build release binaries +./shepherd build --release +``` + +### For Production Deployment + +```sh +# On a runtime-only system +sudo ./shepherd deps install run +./shepherd build --release +sudo ./shepherd install all --user kiosk --prefix /usr + +# Optional: lock down the kiosk user +sudo ./shepherd harden apply --user kiosk +``` + +### For Package Maintainers + +```sh +# Print package lists for your distro +./shepherd deps print build > build-deps.txt +./shepherd deps print run > runtime-deps.txt + +# Install with custom prefix and DESTDIR +make -j$(nproc) # or equivalent +sudo DESTDIR=/tmp/staging ./shepherd install bins --prefix /usr +``` + +## Dependency Sets + +- **build**: Packages needed to compile the Rust code (GTK, Wayland dev libs, etc.) + Rust toolchain via rustup +- **run**: Packages needed to run the compiled binaries (Sway, GTK runtime libs) +- **dev**: Union of build + run + dev-specific tools (git, gdb, strace) + Rust toolchain + +The dev set is computed as the union of all three package lists, automatically deduplicated. + +## Hardening + +The hardening system makes reversible changes to restrict a user to kiosk mode: + +```sh +# Apply hardening +sudo ./shepherd harden apply --user kiosk + +# Check status +sudo ./shepherd harden status --user kiosk + +# Revert all changes +sudo ./shepherd harden revert --user kiosk +``` + +All changes are tracked in `/var/lib/shepherdd/hardening//` for rollback. + +Applied restrictions: +- SSH access denied +- Console (TTY) login restricted +- Sudo access denied +- Shell restricted to Sway sessions only +- Home directory permissions secured + +## Adding New Dependencies + +Edit the appropriate package list in `deps/`: + +- `deps/build.pkgs` - Build-time dependencies +- `deps/run.pkgs` - Runtime dependencies +- `deps/dev.pkgs` - Developer tools + +Format: One package per line, `#` for comments. + +The CI workflow will automatically use these lists. diff --git a/scripts/admin b/scripts/admin new file mode 100755 index 0000000..b0136da --- /dev/null +++ b/scripts/admin @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Admin wrapper - convenience script for administrative tasks +# Provides quick access to install and harden commands + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +usage() { + cat < [options] + +This is a convenience wrapper. All commands are equivalent to: + shepherd install ... + shepherd harden ... + +Commands: + install Install shepherd components + harden Manage user hardening + +Examples: + admin install all --user kiosk + admin harden apply --user kiosk + admin harden revert --user kiosk + +Run 'shepherd install help' or 'shepherd harden help' for details. +EOF +} + +case "${1:-}" in + install|harden) + exec "$SCRIPT_DIR/shepherd" "$@" + ;; + -h|--help|help|"") + usage + ;; + *) + echo "Unknown command: $1" >&2 + echo "Run 'admin help' for usage." >&2 + exit 1 + ;; +esac diff --git a/scripts/deps/build.pkgs b/scripts/deps/build.pkgs new file mode 100644 index 0000000..96d33e7 --- /dev/null +++ b/scripts/deps/build.pkgs @@ -0,0 +1,28 @@ +# Build-time system packages for shepherd-launcher +# These are required to compile the Rust code +# One package per line, comments start with # + +# Core build tools +build-essential +pkg-config + +# GLib/GTK development libraries +libglib2.0-dev +libgtk-4-dev +libadwaita-1-dev +libcairo2-dev +libpango1.0-dev +libgdk-pixbuf-xlib-2.0-dev + +# Wayland development libraries +libwayland-dev +libxkbcommon-dev + +# X11 (for XWayland support) +libx11-dev + +# GObject introspection +libgirepository1.0-dev + +# Layer shell for HUD overlay +libgtk4-layer-shell-dev diff --git a/scripts/deps/dev.pkgs b/scripts/deps/dev.pkgs new file mode 100644 index 0000000..ddd6e81 --- /dev/null +++ b/scripts/deps/dev.pkgs @@ -0,0 +1,11 @@ +# Developer-only packages for shepherd-launcher +# These are extras useful during development +# The full dev set is: build.pkgs + run.pkgs + dev.pkgs +# One package per line, comments start with # + +# Git for version control +git + +# Useful debugging tools +strace +gdb diff --git a/scripts/deps/run.pkgs b/scripts/deps/run.pkgs new file mode 100644 index 0000000..49a2ce2 --- /dev/null +++ b/scripts/deps/run.pkgs @@ -0,0 +1,14 @@ +# Runtime system packages for shepherd-launcher +# These are required to run the compiled binaries +# One package per line, comments start with # + +# Sway compositor (required for kiosk mode) +sway + +# Wayland session utilities +swayidle + +# Runtime libraries (may be pulled in automatically, but explicit is safer) +libgtk-4-1 +libadwaita-1-0 +libgtk4-layer-shell0 diff --git a/scripts/dev b/scripts/dev new file mode 100755 index 0000000..54fcd6c --- /dev/null +++ b/scripts/dev @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Development wrapper - thin convenience script for developers +# Equivalent to: shepherd dev run + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/shepherd" dev run "$@" diff --git a/scripts/lib/build.sh b/scripts/lib/build.sh new file mode 100755 index 0000000..8bfef84 --- /dev/null +++ b/scripts/lib/build.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Build logic for shepherd-launcher +# Wraps cargo build with project-specific settings + +# Get the directory containing this script +BUILD_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common utilities +# shellcheck source=common.sh +source "$BUILD_LIB_DIR/common.sh" + +# Binary names produced by the build +SHEPHERD_BINARIES=( + "shepherdd" + "shepherd-launcher" + "shepherd-hud" +) + +# Get the target directory for binaries +get_target_dir() { + local release="${1:-false}" + local repo_root + repo_root="$(get_repo_root)" + + if [[ "$release" == "true" ]]; then + echo "$repo_root/target/release" + else + echo "$repo_root/target/debug" + fi +} + +# Get the path to a specific binary +get_binary_path() { + local binary="$1" + local release="${2:-false}" + + echo "$(get_target_dir "$release")/$binary" +} + +# Check if all binaries exist +binaries_exist() { + local release="${1:-false}" + local target_dir + target_dir="$(get_target_dir "$release")" + + for binary in "${SHEPHERD_BINARIES[@]}"; do + if [[ ! -x "$target_dir/$binary" ]]; then + return 1 + fi + done + return 0 +} + +# Build the project +build_cargo() { + local release="${1:-false}" + local repo_root + repo_root="$(get_repo_root)" + + verify_repo + require_command cargo rust + + cd "$repo_root" || die "Failed to change directory to $repo_root" + + local build_type + if [[ "$release" == "true" ]]; then + build_type="release" + info "Building shepherd (release mode)..." + cargo build --release + else + build_type="debug" + info "Building shepherd (debug mode)..." + cargo build + fi + + # Verify binaries were created + if ! binaries_exist "$release"; then + die "Build completed but some binaries are missing" + fi + + local target_dir + target_dir="$(get_target_dir "$release")" + + success "Built binaries ($build_type):" + for binary in "${SHEPHERD_BINARIES[@]}"; do + info " $target_dir/$binary" + done +} + +# Clean build artifacts +build_clean() { + local repo_root + repo_root="$(get_repo_root)" + + verify_repo + require_command cargo rust + + cd "$repo_root" || die "Failed to change directory to $repo_root" + + info "Cleaning build artifacts..." + cargo clean + success "Build artifacts cleaned" +} + +# Main build command dispatcher +build_main() { + local release=false + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --release|-r) + release=true + shift + ;; + clean) + build_clean + return + ;; + help|-h|--help) + cat <&2 +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $*" >&2 +} + +error() { + echo -e "${RED}[ERROR]${NC} $*" >&2 +} + +success() { + echo -e "${GREEN}[OK]${NC} $*" >&2 +} + +die() { + error "$@" + exit 1 +} + +# Check if running as root +is_root() { + [[ $EUID -eq 0 ]] +} + +# Require root or exit +require_root() { + if ! is_root; then + die "This command must be run as root (use sudo)" + fi +} + +# Run a command with sudo if not already root +maybe_sudo() { + if is_root; then + "$@" + else + sudo "$@" + fi +} + +# Check if a command exists +command_exists() { + command -v "$1" &>/dev/null +} + +# Require a command or exit with helpful message +require_command() { + local cmd="$1" + local pkg="${2:-$1}" + if ! command_exists "$cmd"; then + die "Required command '$cmd' not found. Install it with: apt install $pkg" + fi +} + +# Get the repository root directory +get_repo_root() { + local script_dir + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + (cd "$script_dir/../.." && pwd) +} + +# Verify we're in the shepherd repository +verify_repo() { + local repo_root + repo_root="$(get_repo_root)" + if [[ ! -f "$repo_root/Cargo.toml" ]]; then + die "Not in shepherd repository (Cargo.toml not found at $repo_root)" + fi + # Check it's the right project + if ! grep -q 'shepherd-launcher-ui' "$repo_root/Cargo.toml" 2>/dev/null; then + die "This doesn't appear to be the shepherd-launcher repository" + fi +} + +# Check Ubuntu version and warn if not supported +check_ubuntu_version() { + local min_version="${1:-25.10}" + + if [[ ! -f /etc/os-release ]]; then + warn "Cannot determine OS version (not Linux or missing /etc/os-release)" + return 0 + fi + + # shellcheck source=/dev/null + source /etc/os-release + + if [[ "${ID:-}" != "ubuntu" ]]; then + warn "This system is not Ubuntu (detected: ${ID:-unknown}). Some features may not work." + return 0 + fi + + local version="${VERSION_ID:-0}" + if [[ "$(printf '%s\n' "$min_version" "$version" | sort -V | head -n1)" != "$min_version" ]]; then + warn "Ubuntu version $version detected. Recommended: $min_version or higher." + fi +} + +# Safe file backup - creates timestamped backup +backup_file() { + local file="$1" + local backup_dir="${2:-}" + + if [[ ! -e "$file" ]]; then + return 0 + fi + + local timestamp + timestamp="$(date +%Y%m%d_%H%M%S)" + local backup_name + + if [[ -n "$backup_dir" ]]; then + mkdir -p "$backup_dir" + backup_name="$backup_dir/$(basename "$file").$timestamp" + else + backup_name="$file.$timestamp.bak" + fi + + cp -a "$file" "$backup_name" + echo "$backup_name" +} + +# Create directory with proper permissions +ensure_dir() { + local dir="$1" + local mode="${2:-0755}" + local owner="${3:-}" + + if [[ ! -d "$dir" ]]; then + mkdir -p "$dir" + chmod "$mode" "$dir" + if [[ -n "$owner" ]]; then + chown "$owner" "$dir" + fi + fi +} + +# Validate username exists +validate_user() { + local user="$1" + if ! id "$user" &>/dev/null; then + die "User '$user' does not exist" + fi +} + +# Get user's home directory +get_user_home() { + local user="$1" + getent passwd "$user" | cut -d: -f6 +} + +# Kill processes matching a pattern (silent if none found) +kill_matching() { + local pattern="$1" + pkill -f "$pattern" 2>/dev/null || true +} + +# Wait for a file/socket to appear +wait_for_file() { + local file="$1" + local timeout="${2:-30}" + local elapsed=0 + + while [[ ! -e "$file" ]] && [[ $elapsed -lt $timeout ]]; do + sleep 0.5 + elapsed=$((elapsed + 1)) + done + + [[ -e "$file" ]] +} diff --git a/scripts/lib/deps.sh b/scripts/lib/deps.sh new file mode 100755 index 0000000..3b62a6b --- /dev/null +++ b/scripts/lib/deps.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +# Dependency management for shepherd-launcher +# Provides functions to read, union, and install package sets + +# Get the directory containing this script +DEPS_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common utilities +# shellcheck source=common.sh +source "$DEPS_LIB_DIR/common.sh" + +# Directory containing package lists +DEPS_DIR="$(get_repo_root)/scripts/deps" + +# Rust installation URL +RUSTUP_URL="https://sh.rustup.rs" + +# Check if Rust is installed +is_rust_installed() { + command_exists rustc && command_exists cargo +} + +# Install Rust via rustup +install_rust() { + if is_rust_installed; then + info "Rust is already installed ($(rustc --version))" + return 0 + fi + + info "Installing Rust via rustup..." + + # Download and run rustup installer + curl --proto '=https' --tlsv1.2 -sSf "$RUSTUP_URL" | sh -s -- -y --profile minimal + + # Source cargo env for current session + # shellcheck source=/dev/null + source "$HOME/.cargo/env" 2>/dev/null || true + + if is_rust_installed; then + success "Rust installed successfully ($(rustc --version))" + else + die "Rust installation failed. Please run: curl --proto '=https' --tlsv1.2 -sSf $RUSTUP_URL | sh" + fi +} + +# Read a package file, stripping comments and empty lines +read_package_file() { + local file="$1" + + if [[ ! -f "$file" ]]; then + die "Package file not found: $file" + fi + + # Strip comments (# to end of line) and empty lines, trim whitespace + grep -v '^\s*#' "$file" | grep -v '^\s*$' | sed 's/#.*//' | tr -s '[:space:]' '\n' | grep -v '^$' +} + +# Get packages for a specific set +get_packages() { + local set_name="$1" + + case "$set_name" in + build) + read_package_file "$DEPS_DIR/build.pkgs" + ;; + run) + read_package_file "$DEPS_DIR/run.pkgs" + ;; + dev) + # Union of all three sets, deduplicated + { + read_package_file "$DEPS_DIR/build.pkgs" + read_package_file "$DEPS_DIR/run.pkgs" + read_package_file "$DEPS_DIR/dev.pkgs" + } | sort -u + ;; + *) + die "Unknown package set: $set_name (valid: build, run, dev)" + ;; + esac +} + +# Print packages for a set (one per line) +deps_print() { + local set_name="${1:-}" + + if [[ -z "$set_name" ]]; then + die "Usage: shepherd deps print " + fi + + get_packages "$set_name" +} + +# Install packages for a set +deps_install() { + local set_name="${1:-}" + + if [[ -z "$set_name" ]]; then + die "Usage: shepherd deps install " + fi + + check_ubuntu_version + + info "Installing $set_name dependencies..." + + # Get the package list + local packages + packages=$(get_packages "$set_name" | tr '\n' ' ') + + if [[ -z "$packages" ]]; then + warn "No packages to install for set: $set_name" + return 0 + fi + + info "Packages: $packages" + + # Install using apt + maybe_sudo apt-get update + # shellcheck disable=SC2086 + maybe_sudo apt-get install -y $packages + + # For build and dev sets, also install Rust + if [[ "$set_name" == "build" ]] || [[ "$set_name" == "dev" ]]; then + install_rust + fi + + success "Installed $set_name dependencies" +} + +# Check if all packages for a set are installed +deps_check() { + local set_name="${1:-}" + + if [[ -z "$set_name" ]]; then + die "Usage: shepherd deps check " + fi + + local packages + packages=$(get_packages "$set_name") + + local missing=() + while IFS= read -r pkg; do + if ! dpkg -l "$pkg" &>/dev/null; then + missing+=("$pkg") + fi + done <<< "$packages" + + # For build and dev sets, also check Rust + if [[ "$set_name" == "build" ]] || [[ "$set_name" == "dev" ]]; then + if ! is_rust_installed; then + warn "Rust is not installed" + return 1 + fi + fi + + if [[ ${#missing[@]} -gt 0 ]]; then + warn "Missing packages: ${missing[*]}" + return 1 + fi + + success "All $set_name dependencies are installed" + return 0 +} + +# Main deps command dispatcher +deps_main() { + local subcmd="${1:-}" + shift || true + + case "$subcmd" in + print) + deps_print "$@" + ;; + install) + deps_install "$@" + ;; + check) + deps_check "$@" + ;; + ""|help|-h|--help) + cat < + +Commands: + print Print packages in the set (one per line) + install Install packages from the set + check Check if all packages from the set are installed + +Package sets: + build Build-time dependencies (+ Rust via rustup) + run Runtime dependencies only + dev All dependencies (build + run + dev extras + Rust) + +Note: The 'build' and 'dev' sets automatically install Rust via rustup. + +Examples: + shepherd deps print build + shepherd deps install dev + shepherd deps check run +EOF + ;; + *) + die "Unknown deps command: $subcmd (try: shepherd deps help)" + ;; + esac +} diff --git a/scripts/lib/harden.sh b/scripts/lib/harden.sh new file mode 100755 index 0000000..0064f9c --- /dev/null +++ b/scripts/lib/harden.sh @@ -0,0 +1,425 @@ +#!/usr/bin/env bash +# User hardening logic for shepherd-launcher +# Applies and reverts kiosk-style user restrictions + +# Get the directory containing this script +HARDEN_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common utilities +# shellcheck source=common.sh +source "$HARDEN_LIB_DIR/common.sh" + +# State directory for hardening rollback +HARDENING_STATE_DIR="/var/lib/shepherdd/hardening" + +# Get the state directory for a user +get_user_state_dir() { + local user="$1" + echo "$HARDENING_STATE_DIR/$user" +} + +# Check if a user is currently hardened +is_hardened() { + local user="$1" + local state_dir + state_dir="$(get_user_state_dir "$user")" + + [[ -f "$state_dir/hardened" ]] +} + +# Save a file for later restoration +save_for_restore() { + local user="$1" + local file="$2" + local state_dir + state_dir="$(get_user_state_dir "$user")" + + local relative_path="${file#/}" + local backup_path="$state_dir/backup/$relative_path" + + mkdir -p "$(dirname "$backup_path")" + + if [[ -e "$file" ]]; then + cp -a "$file" "$backup_path" + echo "exists" > "$backup_path.meta" + else + echo "absent" > "$backup_path.meta" + fi +} + +# Restore a previously saved file +restore_file() { + local user="$1" + local file="$2" + local state_dir + state_dir="$(get_user_state_dir "$user")" + + local relative_path="${file#/}" + local backup_path="$state_dir/backup/$relative_path" + local meta_file="$backup_path.meta" + + if [[ ! -f "$meta_file" ]]; then + warn "No backup metadata for $file, skipping" + return 0 + fi + + local original_state + original_state="$(cat "$meta_file")" + + if [[ "$original_state" == "exists" ]]; then + if [[ -e "$backup_path" ]]; then + cp -a "$backup_path" "$file" + info " Restored: $file" + else + warn "Backup file missing for $file" + fi + else + # File didn't exist originally, remove it + rm -f "$file" + info " Removed: $file (didn't exist before)" + fi +} + +# Record a change action for rollback +record_action() { + local user="$1" + local action="$2" + local target="$3" + local state_dir + state_dir="$(get_user_state_dir "$user")" + + echo "$action|$target" >> "$state_dir/actions.log" +} + +# Apply hardening to a user +harden_apply() { + local user="$1" + + require_root + validate_user "$user" + + local state_dir + state_dir="$(get_user_state_dir "$user")" + local user_home + user_home="$(get_user_home "$user")" + + if is_hardened "$user"; then + warn "User $user is already hardened. Use 'shepherd harden revert' first." + return 0 + fi + + info "Applying hardening to user: $user" + + # Create state directory + mkdir -p "$state_dir/backup" + chmod 0700 "$state_dir" + + # Initialize actions log + : > "$state_dir/actions.log" + + # ========================================================================= + # 1. Set user shell to restricted shell or nologin for non-sway access + # ========================================================================= + info "Configuring user shell..." + + local original_shell + original_shell="$(getent passwd "$user" | cut -d: -f7)" + echo "$original_shell" > "$state_dir/original_shell" + + # Keep bash for sway to work, but we'll restrict other access methods + # The shell restriction is handled by PAM and session limits instead + record_action "$user" "shell" "$original_shell" + + # ========================================================================= + # 2. Configure user's .bashrc to be restricted + # ========================================================================= + info "Configuring shell restrictions..." + + local bashrc="$user_home/.bashrc" + save_for_restore "$user" "$bashrc" + + # Append restriction to bashrc (if not in sway, exit) + cat >> "$bashrc" <<'EOF' + +# Shepherd hardening: restrict to sway session only +if [[ -z "${WAYLAND_DISPLAY:-}" ]] && [[ -z "${SWAYSOCK:-}" ]]; then + echo "This account is restricted to the Shepherd kiosk environment." + exit 1 +fi +EOF + chown "$user:$user" "$bashrc" + record_action "$user" "file" "$bashrc" + + # ========================================================================= + # 3. Disable SSH access for this user + # ========================================================================= + info "Restricting SSH access..." + + local shepherd_sshd_config="/etc/ssh/sshd_config.d/shepherd-$user.conf" + + save_for_restore "$user" "$shepherd_sshd_config" + + # Create a drop-in config to deny this user + mkdir -p /etc/ssh/sshd_config.d + cat > "$shepherd_sshd_config" </dev/null || systemctl is-active --quiet ssh 2>/dev/null; then + systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true + fi + + # ========================================================================= + # 4. Disable virtual console (TTY) access via PAM + # ========================================================================= + info "Restricting console access..." + + local pam_access="/etc/security/access.conf" + local shepherd_access_marker="# Shepherd hardening for user: $user" + + save_for_restore "$user" "$pam_access" + + # Add rule to deny console access (but allow via display managers) + if ! grep -q "$shepherd_access_marker" "$pam_access" 2>/dev/null; then + cat >> "$pam_access" < "$getty_override" < "$sudoers_file" < "$state_dir/home_perms" + + # Set restrictive permissions + chmod 0700 "$user_home" + record_action "$user" "perms" "$user_home" + + # ========================================================================= + # Mark as hardened + # ========================================================================= + date -Iseconds > "$state_dir/hardened" + echo "$user" > "$state_dir/user" + + success "Hardening applied to user: $user" + info "" + info "The following restrictions are now active:" + info " - SSH access denied" + info " - Console (TTY) login restricted" + info " - Sudo access denied" + info " - Shell restricted to Sway sessions" + info " - Home directory secured (mode 0700)" + info "" + info "To revert: shepherd harden revert --user $user" +} + +# Revert hardening from a user +harden_revert() { + local user="$1" + + require_root + validate_user "$user" + + local state_dir + state_dir="$(get_user_state_dir "$user")" + local user_home + user_home="$(get_user_home "$user")" + + if ! is_hardened "$user"; then + warn "User $user is not currently hardened." + return 0 + fi + + info "Reverting hardening for user: $user" + + # ========================================================================= + # Restore all saved files + # ========================================================================= + if [[ -f "$state_dir/actions.log" ]]; then + while IFS='|' read -r action target; do + case "$action" in + file) + restore_file "$user" "$target" + ;; + perms) + if [[ -f "$state_dir/home_perms" ]]; then + local original_perms + original_perms="$(cat "$state_dir/home_perms")" + chmod "$original_perms" "$target" + info " Restored permissions on: $target" + fi + ;; + shell) + # Shell wasn't changed, nothing to revert + ;; + esac + done < "$state_dir/actions.log" + fi + + # ========================================================================= + # Reload services that may have been affected + # ========================================================================= + if systemctl is-active --quiet sshd 2>/dev/null || systemctl is-active --quiet ssh 2>/dev/null; then + systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true + fi + + # ========================================================================= + # Clean up state directory + # ========================================================================= + rm -rf "$state_dir" + + success "Hardening reverted for user: $user" + info "" + info "All restrictions have been removed. The user can now:" + info " - Access via SSH" + info " - Login at console" + info " - Use sudo (if previously allowed)" +} + +# Show hardening status +harden_status() { + local user="$1" + + validate_user "$user" + + local state_dir + state_dir="$(get_user_state_dir "$user")" + + if is_hardened "$user"; then + local hardened_date + hardened_date="$(cat "$state_dir/hardened")" + echo "User '$user' is HARDENED (since $hardened_date)" + + if [[ -f "$state_dir/actions.log" ]]; then + echo "" + echo "Applied restrictions:" + while IFS='|' read -r action target; do + echo " - $action: $target" + done < "$state_dir/actions.log" + fi + else + echo "User '$user' is NOT hardened" + fi +} + +# Main harden command dispatcher +harden_main() { + local subcmd="${1:-}" + shift || true + + local user="" + + # Parse remaining arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --user) + user="$2" + shift 2 + ;; + *) + die "Unknown option: $1" + ;; + esac + done + + case "$subcmd" in + apply) + if [[ -z "$user" ]]; then + die "Usage: shepherd harden apply --user USER" + fi + harden_apply "$user" + ;; + revert) + if [[ -z "$user" ]]; then + die "Usage: shepherd harden revert --user USER" + fi + harden_revert "$user" + ;; + status) + if [[ -z "$user" ]]; then + die "Usage: shepherd harden status --user USER" + fi + harden_status "$user" + ;; + ""|help|-h|--help) + cat < --user USER + +Commands: + apply Apply kiosk hardening to a user + revert Revert hardening and restore original state + status Show hardening status for a user + +Options: + --user USER Target user for hardening operations (required) + +Hardening includes: + - Denying SSH access + - Restricting console (TTY) login + - Denying sudo access + - Restricting shell to Sway sessions only + - Securing home directory permissions + +State is preserved in: $HARDENING_STATE_DIR// + +Examples: + shepherd harden apply --user kiosk + shepherd harden status --user kiosk + shepherd harden revert --user kiosk +EOF + ;; + *) + die "Unknown harden command: $subcmd (try: shepherd harden help)" + ;; + esac +} diff --git a/scripts/lib/install.sh b/scripts/lib/install.sh new file mode 100755 index 0000000..8c8ae0b --- /dev/null +++ b/scripts/lib/install.sh @@ -0,0 +1,306 @@ +#!/usr/bin/env bash +# Installation logic for shepherd-launcher +# Handles binary installation, config deployment, and desktop entry setup + +# Get the directory containing this script +INSTALL_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common utilities +# shellcheck source=common.sh +source "$INSTALL_LIB_DIR/common.sh" + +# Source build utilities for binary paths +# shellcheck source=build.sh +source "$INSTALL_LIB_DIR/build.sh" + +# Default installation paths +DEFAULT_PREFIX="/usr/local" +DEFAULT_BINDIR="bin" + +# Standard sway config location +SWAY_CONFIG_DIR="/etc/sway" +SHEPHERD_SWAY_CONFIG="shepherd.conf" + +# Desktop entry location +DESKTOP_ENTRY_DIR="share/wayland-sessions" +DESKTOP_ENTRY_NAME="shepherd.desktop" + +# Install release binaries +install_bins() { + local prefix="${1:-$DEFAULT_PREFIX}" + local destdir="${DESTDIR:-}" + + require_root + + local bindir="$destdir$prefix/$DEFAULT_BINDIR" + local target_dir + target_dir="$(get_target_dir true)" + + # Ensure release build exists + if ! binaries_exist true; then + die "Release binaries not found. Run 'shepherd build --release' first." + fi + + info "Installing binaries to $bindir..." + + ensure_dir "$bindir" 0755 + + for binary in "${SHEPHERD_BINARIES[@]}"; do + local src="$target_dir/$binary" + local dst="$bindir/$binary" + + info " Installing $binary..." + install -m 0755 "$src" "$dst" + done + + success "Installed binaries to $bindir" +} + +# Install the sway configuration +install_sway_config() { + local destdir="${DESTDIR:-}" + local repo_root + repo_root="$(get_repo_root)" + + require_root + + local src_config="$repo_root/sway.conf" + local dst_dir="$destdir$SWAY_CONFIG_DIR" + local dst_config="$dst_dir/$SHEPHERD_SWAY_CONFIG" + + if [[ ! -f "$src_config" ]]; then + die "Source sway.conf not found at $src_config" + fi + + info "Installing sway configuration to $dst_config..." + + ensure_dir "$dst_dir" 0755 + + # Create a production version of the sway config + # Replace debug paths with installed paths + local prefix="${1:-$DEFAULT_PREFIX}" + local bindir="$prefix/$DEFAULT_BINDIR" + + # Copy and modify the config for production use + sed \ + -e "s|./target/debug/shepherd-launcher|$bindir/shepherd-launcher|g" \ + -e "s|./target/debug/shepherd-hud|$bindir/shepherd-hud|g" \ + -e "s|./target/debug/shepherdd|$bindir/shepherdd|g" \ + -e "s|./config.example.toml|/etc/shepherd/config.toml|g" \ + -e "s|-c ./sway.conf|-c $dst_config|g" \ + "$src_config" > "$dst_config" + + chmod 0644 "$dst_config" + + success "Installed sway configuration" +} + +# Install desktop entry for display manager +install_desktop_entry() { + local prefix="${1:-$DEFAULT_PREFIX}" + local destdir="${DESTDIR:-}" + + require_root + + local dst_dir="$destdir$prefix/$DESKTOP_ENTRY_DIR" + local dst_entry="$dst_dir/$DESKTOP_ENTRY_NAME" + + info "Installing desktop entry to $dst_entry..." + + ensure_dir "$dst_dir" 0755 + + cat > "$dst_entry" < [OPTIONS] + +Commands: + bins Install release binaries + config Deploy user configuration + sway-config Install sway configuration + desktop-entry Install display manager desktop entry + system-config Install system-wide configuration + all Install everything + +Options: + --user USER Target user for config deployment (required for config/all) + --prefix PREFIX Installation prefix (default: $DEFAULT_PREFIX) + --source CONFIG Source config file (default: config.example.toml) + +Environment: + DESTDIR Installation root for packaging (default: empty) + +Examples: + shepherd install bins --prefix /usr/local + shepherd install config --user kiosk + shepherd install all --user kiosk --prefix /usr +EOF + ;; + *) + die "Unknown install command: $subcmd (try: shepherd install help)" + ;; + esac +} diff --git a/scripts/lib/sway.sh b/scripts/lib/sway.sh new file mode 100755 index 0000000..2aeb900 --- /dev/null +++ b/scripts/lib/sway.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# Sway compositor helpers for shepherd-launcher +# Handles nested sway execution for development and production + +# Get the directory containing this script +SWAY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common utilities +# shellcheck source=common.sh +source "$SWAY_LIB_DIR/common.sh" + +# Source build utilities for binary paths +# shellcheck source=build.sh +source "$SWAY_LIB_DIR/build.sh" + +# PID of the running sway process (for cleanup) +SWAY_PID="" + +# Default directories +DEFAULT_DEV_RUNTIME="./dev-runtime" +DEFAULT_DATA_DIR="$DEFAULT_DEV_RUNTIME/data" +DEFAULT_SOCKET_PATH="$DEFAULT_DEV_RUNTIME/shepherd.sock" + +# Cleanup function for sway processes +sway_cleanup() { + info "Cleaning up sway session..." + + # Kill the nested sway - this will clean up everything inside it + if [[ -n "${SWAY_PID:-}" ]]; then + kill "$SWAY_PID" 2>/dev/null || true + fi + + # Explicitly kill any shepherd processes that might have escaped + kill_matching "shepherdd" + kill_matching "shepherd-launcher" + kill_matching "shepherd-hud" + + # Remove socket + if [[ -n "${SHEPHERD_SOCKET:-}" ]]; then + rm -f "$SHEPHERD_SOCKET" + fi +} + +# Kill any existing dev instances +sway_kill_existing() { + info "Cleaning up any existing dev instances..." + kill_matching "sway -c.*sway.conf" + kill_matching "shepherdd" + kill_matching "shepherd-launcher" + kill_matching "shepherd-hud" + + # Remove stale socket if it exists + if [[ -n "${SHEPHERD_SOCKET:-}" ]] && [[ -e "$SHEPHERD_SOCKET" ]]; then + rm -f "$SHEPHERD_SOCKET" + fi + + # Brief pause to allow cleanup + sleep 0.5 +} + +# Set up environment for shepherd binaries +sway_setup_env() { + local data_dir="${1:-$DEFAULT_DATA_DIR}" + local socket_path="${2:-$DEFAULT_SOCKET_PATH}" + + # Create directories + mkdir -p "$data_dir" + + # Export environment variables + export SHEPHERD_SOCKET="$socket_path" + export SHEPHERD_DATA_DIR="$data_dir" +} + +# Generate a sway config for development +# Uses debug binaries and development paths +sway_generate_dev_config() { + local repo_root + repo_root="$(get_repo_root)" + + # Use the existing sway.conf as template + local sway_config="$repo_root/sway.conf" + + if [[ ! -f "$sway_config" ]]; then + die "sway.conf not found at $sway_config" + fi + + # Return path to the sway config (we use the existing one for dev) + echo "$sway_config" +} + +# Start a nested sway session for development +sway_start_nested() { + local sway_config="$1" + + require_command sway + + info "Starting nested sway session..." + + # Set up cleanup trap + trap sway_cleanup EXIT + + # Start sway with wayland backend (nested in current session) + WLR_BACKENDS=wayland WLR_LIBINPUT_NO_DEVICES=1 sway -c "$sway_config" & + SWAY_PID=$! + + info "Sway started with PID $SWAY_PID" + + # Wait for sway to exit + wait "$SWAY_PID" +} + +# Run a development session (build + nested sway) +sway_dev_run() { + local repo_root + repo_root="$(get_repo_root)" + + verify_repo + cd "$repo_root" || die "Failed to change directory to $repo_root" + + # Set up environment + sway_setup_env "$DEFAULT_DATA_DIR" "$DEFAULT_SOCKET_PATH" + + # Kill any existing instances + sway_kill_existing + + # Build debug binaries + info "Building shepherd binaries..." + build_cargo false + + # Get sway config + local sway_config + sway_config="$(sway_generate_dev_config)" + + # Start nested sway (blocking) + sway_start_nested "$sway_config" +} + +# Main sway command dispatcher (internal use) +sway_main() { + local subcmd="${1:-}" + shift || true + + case "$subcmd" in + run) + sway_dev_run "$@" + ;; + *) + # Default to run for backwards compatibility + sway_dev_run "$@" + ;; + esac +} diff --git a/scripts/shepherd b/scripts/shepherd new file mode 100755 index 0000000..6cf44a5 --- /dev/null +++ b/scripts/shepherd @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# Shepherd CLI - Main entrypoint for shepherd-launcher tooling +# Provides unified interface for build, development, installation, and hardening + +set -euo pipefail + +# Get the directory containing this script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LIB_DIR="$SCRIPT_DIR/lib" + +# Source shared libraries +# shellcheck source=lib/common.sh +source "$LIB_DIR/common.sh" +# shellcheck source=lib/deps.sh +source "$LIB_DIR/deps.sh" +# shellcheck source=lib/build.sh +source "$LIB_DIR/build.sh" +# shellcheck source=lib/sway.sh +source "$LIB_DIR/sway.sh" +# shellcheck source=lib/install.sh +source "$LIB_DIR/install.sh" +# shellcheck source=lib/harden.sh +source "$LIB_DIR/harden.sh" + +# Version +VERSION="0.1.0" + +# Print usage information +usage() { + cat < [subcommand] [options] + +Commands: + deps Manage system dependencies + build Build shepherd binaries + dev Development commands + install Install shepherd to the system + harden User hardening for kiosk mode + +Development: + shepherd deps install dev Install all development dependencies + shepherd build Build debug binaries + shepherd dev run Build and run in nested sway + +CI/Build: + shepherd deps install build Install build-only dependencies + shepherd build --release Build release binaries + +Runtime: + shepherd deps install run Install runtime dependencies only + +Installation: + shepherd install all --user USER Full installation + shepherd install bins --prefix /usr Install binaries only + shepherd install config --user USER Deploy user config + +Hardening: + shepherd harden apply --user USER Apply kiosk restrictions + shepherd harden revert --user USER Remove restrictions + +Options: + -h, --help Show this help message + -V, --version Show version + +Run 'shepherd help' for detailed command information. +EOF +} + +# Main entry point +main() { + local cmd="${1:-}" + shift || true + + case "$cmd" in + deps) + deps_main "$@" + ;; + build) + build_main "$@" + ;; + dev) + local subcmd="${1:-}" + shift || true + case "$subcmd" in + run) + sway_dev_run "$@" + ;; + ""|help|-h|--help) + cat < + +Commands: + run Build and run shepherd in a nested sway session + +Examples: + shepherd dev run +EOF + ;; + *) + die "Unknown dev command: $subcmd (try: shepherd dev help)" + ;; + esac + ;; + install) + install_main "$@" + ;; + harden) + harden_main "$@" + ;; + -h|--help|help|"") + usage + ;; + -V|--version|version) + echo "shepherd $VERSION" + ;; + *) + error "Unknown command: $cmd" + echo "" + usage + exit 1 + ;; + esac +} + +# Run main with all arguments +main "$@"