mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-31 16:49:34 +00:00
* fix(setup): check cloudflared binary and validate tunnel token (#418) The Cloudflare tunnel setup accepted tokens blindly without checking if cloudflared was installed or if the token was valid. Now: - Checks for cloudflared on PATH before accepting a token, with install instructions if missing (user can continue anyway) - Validates token format (base64-decoded JSON with account/tunnel fields) with a warning if malformed (user can override) - Replaces misleading "will start automatically at boot" with honest instructions for starting the tunnel and installing as a service - Reuses binary_exists() from skills::gating (promoted to pub(crate)) for cross-platform PATH lookup Closes #418 Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: reuse cloudflared_found instead of redundant binary_exists call Address review feedback: the binary check result was already stored in cloudflared_found from earlier in the function. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
168 lines
5.2 KiB
Rust
168 lines
5.2 KiB
Rust
//! Requirements gating for skills.
|
|
//!
|
|
//! Checks that a skill's declared requirements (binaries, environment variables,
|
|
//! config files) are satisfied before the skill is loaded.
|
|
|
|
use crate::skills::GatingRequirements;
|
|
|
|
/// Result of a gating check.
|
|
#[derive(Debug)]
|
|
pub struct GatingResult {
|
|
/// Whether all requirements passed.
|
|
pub passed: bool,
|
|
/// Descriptions of failed requirements.
|
|
pub failures: Vec<String>,
|
|
}
|
|
|
|
/// Async wrapper around [`check_requirements_sync`] that offloads blocking
|
|
/// subprocess calls (`which`/`where`) to a blocking thread pool via
|
|
/// `tokio::task::spawn_blocking`.
|
|
pub async fn check_requirements(requirements: &GatingRequirements) -> GatingResult {
|
|
let requirements = requirements.clone();
|
|
tokio::task::spawn_blocking(move || check_requirements_sync(&requirements))
|
|
.await
|
|
.unwrap_or_else(|e| {
|
|
let message = if e.is_panic() {
|
|
format!("gating check panicked: {}", e)
|
|
} else if e.is_cancelled() {
|
|
format!("gating check task was cancelled: {}", e)
|
|
} else {
|
|
format!("gating check failed to join: {}", e)
|
|
};
|
|
tracing::error!("{}", message);
|
|
GatingResult {
|
|
passed: false,
|
|
failures: vec![message],
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Check whether gating requirements are satisfied (synchronous).
|
|
///
|
|
/// - `bins`: checks that each binary is findable via `which` (PATH lookup).
|
|
/// - `env`: checks that each environment variable is set.
|
|
/// - `config`: checks that each config file path exists.
|
|
///
|
|
/// Skills that fail gating should be logged and skipped, not loaded.
|
|
///
|
|
/// This is the synchronous implementation; prefer the async [`check_requirements`]
|
|
/// wrapper when calling from async contexts to avoid blocking the tokio runtime.
|
|
pub fn check_requirements_sync(requirements: &GatingRequirements) -> GatingResult {
|
|
let mut failures = Vec::new();
|
|
|
|
for bin in &requirements.bins {
|
|
if !binary_exists(bin) {
|
|
failures.push(format!("required binary not found: {}", bin));
|
|
}
|
|
}
|
|
|
|
for var in &requirements.env {
|
|
if std::env::var(var).is_err() {
|
|
failures.push(format!("required env var not set: {}", var));
|
|
}
|
|
}
|
|
|
|
for path in &requirements.config {
|
|
if !std::path::Path::new(path).exists() {
|
|
failures.push(format!("required config not found: {}", path));
|
|
}
|
|
}
|
|
|
|
GatingResult {
|
|
passed: failures.is_empty(),
|
|
failures,
|
|
}
|
|
}
|
|
|
|
/// Check if a binary exists on PATH using `std::process::Command`.
|
|
pub(crate) fn binary_exists(name: &str) -> bool {
|
|
#[cfg(unix)]
|
|
{
|
|
std::process::Command::new("which")
|
|
.arg(name)
|
|
.stdout(std::process::Stdio::null())
|
|
.stderr(std::process::Stdio::null())
|
|
.status()
|
|
.is_ok_and(|s| s.success())
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
std::process::Command::new("where")
|
|
.arg(name)
|
|
.stdout(std::process::Stdio::null())
|
|
.stderr(std::process::Stdio::null())
|
|
.status()
|
|
.is_ok_and(|s| s.success())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_empty_requirements_pass() {
|
|
let req = GatingRequirements::default();
|
|
let result = check_requirements_sync(&req);
|
|
assert!(result.passed);
|
|
assert!(result.failures.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_missing_binary_fails() {
|
|
let req = GatingRequirements {
|
|
bins: vec!["__ironclaw_nonexistent_binary_xyz__".to_string()],
|
|
..Default::default()
|
|
};
|
|
let result = check_requirements_sync(&req);
|
|
assert!(!result.passed);
|
|
assert_eq!(result.failures.len(), 1);
|
|
assert!(result.failures[0].contains("binary not found"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_missing_env_var_fails() {
|
|
let req = GatingRequirements {
|
|
env: vec!["__IRONCLAW_TEST_NONEXISTENT_VAR__".to_string()],
|
|
..Default::default()
|
|
};
|
|
let result = check_requirements_sync(&req);
|
|
assert!(!result.passed);
|
|
assert!(result.failures[0].contains("env var not set"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_present_env_var_passes() {
|
|
// PATH is always set on both Unix and Windows
|
|
let req = GatingRequirements {
|
|
env: vec!["PATH".to_string()],
|
|
..Default::default()
|
|
};
|
|
let result = check_requirements_sync(&req);
|
|
assert!(result.passed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_missing_config_fails() {
|
|
let req = GatingRequirements {
|
|
config: vec!["/nonexistent/path/ironclaw_test.conf".to_string()],
|
|
..Default::default()
|
|
};
|
|
let result = check_requirements_sync(&req);
|
|
assert!(!result.passed);
|
|
assert!(result.failures[0].contains("config not found"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_mixed_requirements() {
|
|
let req = GatingRequirements {
|
|
bins: vec!["__no_such_bin__".to_string()],
|
|
env: vec!["__NO_SUCH_VAR__".to_string()],
|
|
config: vec!["/no/such/file".to_string()],
|
|
};
|
|
let result = check_requirements_sync(&req);
|
|
assert!(!result.passed);
|
|
assert_eq!(result.failures.len(), 3);
|
|
}
|
|
}
|