Merge pull request #19 from aarmea/u/aarmea/fix/graceful-config

Add configuration validation
This commit is contained in:
Albert Armea 2026-01-04 21:11:28 -05:00 committed by GitHub
commit dc58817aea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 367 additions and 7 deletions

View file

@ -5,6 +5,10 @@ edition.workspace = true
license.workspace = true license.workspace = true
description = "Configuration parsing and validation for shepherdd" description = "Configuration parsing and validation for shepherdd"
[[bin]]
name = "validate-config"
path = "src/bin/validate-config.rs"
[dependencies] [dependencies]
shepherd-util = { workspace = true } shepherd-util = { workspace = true }
shepherd-api = { workspace = true } shepherd-api = { workspace = true }

View file

@ -86,8 +86,8 @@ max_run_seconds = 3600 # 1 hour
use shepherd_config::{load_config, parse_config, Policy}; use shepherd_config::{load_config, parse_config, Policy};
use std::path::Path; use std::path::Path;
// Load from file // Load from file (typically ~/.config/shepherd/config.toml)
let policy = load_config("/etc/shepherdd/config.toml")?; let policy = load_config("config.toml")?;
// Parse from string // Parse from string
let toml_content = std::fs::read_to_string("config.toml")?; let toml_content = std::fs::read_to_string("config.toml")?;

View file

@ -0,0 +1,101 @@
//! Config validation CLI tool
//!
//! Validates a shepherdd configuration file and reports any errors.
use shepherd_api::EntryKind;
use shepherd_util::default_config_path;
use std::path::PathBuf;
use std::process::ExitCode;
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect();
let config_path = match args.get(1) {
Some(path) => PathBuf::from(path),
None => {
let default_path = default_config_path();
eprintln!("Usage: validate-config [config-file]");
eprintln!();
eprintln!("Validates a shepherdd configuration file.");
eprintln!();
eprintln!("If no path is provided, uses: {}", default_path.display());
eprintln!();
eprintln!("Example:");
eprintln!(" validate-config {}", default_path.display());
eprintln!(" validate-config config.example.toml");
return ExitCode::from(2);
}
};
// Check file exists
if !config_path.exists() {
eprintln!("Error: Configuration file not found: {}", config_path.display());
return ExitCode::from(1);
}
// Try to load and validate
match shepherd_config::load_config(&config_path) {
Ok(policy) => {
println!("✓ Configuration is valid");
println!();
println!("Summary:");
println!(" Config version: {}", shepherd_config::CURRENT_CONFIG_VERSION);
println!(" Entries: {}", policy.entries.len());
// Show entry summary
if !policy.entries.is_empty() {
println!();
println!("Entries:");
for entry in &policy.entries {
let kind_str = match &entry.kind {
EntryKind::Process { command, .. } => {
format!("process ({})", command)
}
EntryKind::Snap { snap_name, .. } => {
format!("snap ({})", snap_name)
}
EntryKind::Vm { driver, .. } => {
format!("vm ({})", driver)
}
EntryKind::Media { library_id, .. } => {
format!("media ({})", library_id)
}
EntryKind::Custom { type_name, .. } => {
format!("custom ({})", type_name)
}
};
println!(" - {} [{}]: {}", entry.id.as_str(), kind_str, entry.label);
}
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("✗ Configuration validation failed");
eprintln!();
match &e {
shepherd_config::ConfigError::ReadError(io_err) => {
eprintln!("Failed to read file: {}", io_err);
}
shepherd_config::ConfigError::ParseError(parse_err) => {
eprintln!("TOML parse error:");
eprintln!(" {}", parse_err);
}
shepherd_config::ConfigError::ValidationFailed { errors } => {
eprintln!("Validation errors ({}):", errors.len());
for err in errors {
eprintln!(" - {}", err);
}
}
shepherd_config::ConfigError::UnsupportedVersion(ver) => {
eprintln!(
"Unsupported config version: {} (expected {})",
ver,
shepherd_config::CURRENT_CONFIG_VERSION
);
}
}
ExitCode::from(1)
}
}
}

View file

@ -115,6 +115,37 @@ pub fn socket_dir() -> PathBuf {
}) })
} }
/// Configuration subdirectory name (uses "shepherd" not "shepherdd")
const CONFIG_APP_DIR: &str = "shepherd";
/// Configuration filename
const CONFIG_FILENAME: &str = "config.toml";
/// Get the default configuration file path.
///
/// Returns `$XDG_CONFIG_HOME/shepherd/config.toml` or `~/.config/shepherd/config.toml`
pub fn default_config_path() -> PathBuf {
// Try XDG_CONFIG_HOME first
if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME") {
return PathBuf::from(config_home)
.join(CONFIG_APP_DIR)
.join(CONFIG_FILENAME);
}
// Fallback to ~/.config/shepherd/config.toml
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home)
.join(".config")
.join(CONFIG_APP_DIR)
.join(CONFIG_FILENAME);
}
// Last resort (unlikely to be valid, but provides a fallback)
PathBuf::from("/etc")
.join(CONFIG_APP_DIR)
.join(CONFIG_FILENAME)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -145,4 +176,11 @@ mod tests {
let dir = socket_dir(); let dir = socket_dir();
assert_eq!(socket.parent().unwrap(), dir); assert_eq!(socket.parent().unwrap(), dir);
} }
#[test]
fn config_path_contains_shepherd() {
let path = default_config_path();
assert!(path.to_string_lossy().contains("shepherd"));
assert!(path.to_string_lossy().ends_with("config.toml"));
}
} }

View file

@ -65,7 +65,7 @@ shepherdd --log-level debug
| Option | Default | Description | | Option | Default | Description |
|--------|---------|-------------| |--------|---------|-------------|
| `-c, --config` | `/etc/shepherdd/config.toml` | Configuration file path | | `-c, --config` | `~/.config/shepherd/config.toml` | Configuration file path |
| `-s, --socket` | From config | IPC socket path | | `-s, --socket` | From config | IPC socket path |
| `-d, --data-dir` | From config | Data directory | | `-d, --data-dir` | From config | Data directory |
| `-l, --log-level` | `info` | Log verbosity | | `-l, --log-level` | `info` | Log verbosity |

View file

@ -21,7 +21,7 @@ use shepherd_host_api::{HostAdapter, HostEvent, StopMode as HostStopMode, Volume
use shepherd_host_linux::{LinuxHost, LinuxVolumeController}; use shepherd_host_linux::{LinuxHost, LinuxVolumeController};
use shepherd_ipc::{IpcServer, ServerMessage}; use shepherd_ipc::{IpcServer, ServerMessage};
use shepherd_store::{AuditEvent, AuditEventType, SqliteStore, Store}; use shepherd_store::{AuditEvent, AuditEventType, SqliteStore, Store};
use shepherd_util::{ClientId, MonotonicInstant, RateLimiter}; use shepherd_util::{default_config_path, ClientId, MonotonicInstant, RateLimiter};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -35,8 +35,8 @@ use tracing_subscriber::EnvFilter;
#[command(name = "shepherdd")] #[command(name = "shepherdd")]
#[command(about = "Policy enforcement service for child-focused computing", long_about = None)] #[command(about = "Policy enforcement service for child-focused computing", long_about = None)]
struct Args { struct Args {
/// Configuration file path /// Configuration file path (default: ~/.config/shepherd/config.toml)
#[arg(short, long, default_value = "/etc/shepherdd/config.toml")] #[arg(short, long, default_value_os_t = default_config_path())]
config: PathBuf, config: PathBuf,
/// Socket path override (or set SHEPHERD_SOCKET env var) /// Socket path override (or set SHEPHERD_SOCKET env var)

View file

@ -15,6 +15,9 @@ This directory contains the unified script system for shepherd-launcher.
# Building # Building
./shepherd build [--release] ./shepherd build [--release]
# Configuration
./shepherd config validate [path]
# Development # Development
./shepherd dev run ./shepherd dev run
@ -39,6 +42,7 @@ scripts/
│ ├── common.sh # Logging, error handling, sudo helpers │ ├── common.sh # Logging, error handling, sudo helpers
│ ├── deps.sh # Dependency management │ ├── deps.sh # Dependency management
│ ├── build.sh # Cargo build logic │ ├── build.sh # Cargo build logic
│ ├── config.sh # Configuration validation
│ ├── sway.sh # Nested sway execution │ ├── sway.sh # Nested sway execution
│ ├── install.sh # Installation logic │ ├── install.sh # Installation logic
│ └── harden.sh # User hardening/unhardening │ └── harden.sh # User hardening/unhardening

202
scripts/lib/config.sh Normal file
View file

@ -0,0 +1,202 @@
#!/usr/bin/env bash
# Configuration validation logic for shepherd-launcher
# Validates shepherdd configuration files
# Get the directory containing this script
CONFIG_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Source common utilities
# shellcheck source=common.sh
source "$CONFIG_LIB_DIR/common.sh"
# Default configuration paths
# Uses XDG_CONFIG_HOME or ~/.config/shepherd/config.toml
get_default_config_path() {
if [[ -n "${XDG_CONFIG_HOME:-}" ]]; then
echo "$XDG_CONFIG_HOME/shepherd/config.toml"
else
echo "$HOME/.config/shepherd/config.toml"
fi
}
EXAMPLE_CONFIG_NAME="config.example.toml"
# Get path to the validate-config binary
get_validate_binary() {
local release="${1:-false}"
local repo_root
repo_root="$(get_repo_root)"
if [[ "$release" == "true" ]]; then
echo "$repo_root/target/release/validate-config"
else
echo "$repo_root/target/debug/validate-config"
fi
}
# Build the validate-config binary if needed
build_validate_binary() {
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"
if [[ "$release" == "true" ]]; then
info "Building validate-config (release mode)..."
cargo build --release --bin validate-config
else
info "Building validate-config..."
cargo build --bin validate-config
fi
}
# Ensure the validate binary exists, building if necessary
ensure_validate_binary() {
local release="${1:-false}"
local binary
binary="$(get_validate_binary "$release")"
if [[ ! -x "$binary" ]]; then
build_validate_binary "$release"
fi
echo "$binary"
}
# Validate a configuration file
validate_config_file() {
local config_path="$1"
local release="${2:-false}"
# Build/find the validator
local binary
binary="$(ensure_validate_binary "$release")"
# Run validation
"$binary" "$config_path"
}
# Show config help
config_usage() {
cat <<EOF
Usage: shepherd config <command> [options]
Commands:
validate [path] Validate a configuration file
help Show this help message
Options:
--release Use release build of validator
The validate command checks a configuration file for:
- Valid TOML syntax
- Correct config_version
- Valid entry definitions
- Valid time windows and availability specs
- Warning thresholds that make sense with limits
Examples:
# Validate the installed config
shepherd config validate
# Validate a specific file
shepherd config validate /path/to/config.toml
# Validate the example config in the repo
shepherd config validate config.example.toml
Default paths:
Installed: $(get_default_config_path)
Example: \$REPO_ROOT/$EXAMPLE_CONFIG_NAME
EOF
}
# Main entry point for config commands
config_main() {
local subcmd="${1:-}"
shift || true
case "$subcmd" in
validate)
local config_path=""
local release="false"
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--release)
release="true"
shift
;;
-h|--help)
config_usage
return 0
;;
-*)
die "Unknown option: $1"
;;
*)
if [[ -z "$config_path" ]]; then
config_path="$1"
else
die "Too many arguments"
fi
shift
;;
esac
done
# Determine config path
if [[ -z "$config_path" ]]; then
# Try default path first
local default_config
default_config="$(get_default_config_path)"
if [[ -f "$default_config" ]]; then
config_path="$default_config"
info "Using installed config: $config_path"
else
# Fall back to example in repo
local repo_root
repo_root="$(get_repo_root)"
local example_path="$repo_root/$EXAMPLE_CONFIG_NAME"
if [[ -f "$example_path" ]]; then
config_path="$example_path"
info "Using example config: $config_path"
else
die "No config file found. Specify a path or install shepherdd."
fi
fi
fi
# Resolve relative paths
if [[ ! "$config_path" = /* ]]; then
config_path="$(pwd)/$config_path"
fi
# Check file exists
if [[ ! -f "$config_path" ]]; then
die "Configuration file not found: $config_path"
fi
# Validate
info "Validating: $config_path"
echo ""
validate_config_file "$config_path" "$release"
;;
""|help|-h|--help)
config_usage
;;
*)
error "Unknown config command: $subcmd"
echo ""
config_usage
exit 1
;;
esac
}

View file

@ -21,6 +21,8 @@ source "$LIB_DIR/sway.sh"
source "$LIB_DIR/install.sh" source "$LIB_DIR/install.sh"
# shellcheck source=lib/harden.sh # shellcheck source=lib/harden.sh
source "$LIB_DIR/harden.sh" source "$LIB_DIR/harden.sh"
# shellcheck source=lib/config.sh
source "$LIB_DIR/config.sh"
# Version # Version
VERSION="0.1.0" VERSION="0.1.0"
@ -35,6 +37,7 @@ Usage: shepherd <command> [subcommand] [options]
Commands: Commands:
deps Manage system dependencies deps Manage system dependencies
build Build shepherd binaries build Build shepherd binaries
config Configuration validation
dev Development commands dev Development commands
install Install shepherd to the system install Install shepherd to the system
harden User hardening for kiosk mode harden User hardening for kiosk mode
@ -60,6 +63,10 @@ Hardening:
shepherd harden apply --user USER Apply kiosk restrictions shepherd harden apply --user USER Apply kiosk restrictions
shepherd harden revert --user USER Remove restrictions shepherd harden revert --user USER Remove restrictions
Configuration:
shepherd config validate Validate installed config
shepherd config validate <path> Validate a specific file
Options: Options:
-h, --help Show this help message -h, --help Show this help message
-V, --version Show version -V, --version Show version
@ -80,6 +87,9 @@ main() {
build) build)
build_main "$@" build_main "$@"
;; ;;
config)
config_main "$@"
;;
dev) dev)
local subcmd="${1:-}" local subcmd="${1:-}"
shift || true shift || true

View file

@ -145,7 +145,8 @@ workspace 1 output *
# Start shepherdd FIRST - it needs to create the socket before HUD/launcher connect # Start shepherdd FIRST - it needs to create the socket before HUD/launcher connect
# Running inside sway ensures all spawned processes use the nested compositor # Running inside sway ensures all spawned processes use the nested compositor
# Shepherdd handles SIGHUP for graceful shutdown when sway exits # Shepherdd handles SIGHUP for graceful shutdown when sway exits
exec ./target/debug/shepherdd -c ./config.example.toml # If shepherdd fails to start (e.g., config error), exit sway to prevent a broken state
exec sh -c './target/debug/shepherdd -c ./config.example.toml || swaymsg exit'
# Give shepherdd a moment to initialize, then start UI components # Give shepherdd a moment to initialize, then start UI components
# Start the shepherd-hud (time remaining overlay) # Start the shepherd-hud (time remaining overlay)