rivox 1.0.0

Universal polyglot build coordination layer for Python, Rust, and Node monorepos
Documentation
use super::local::LocalCas;
use super::reapi_exec::{
    ActionResult, ActionSpec, CommandSpec, DigestVal, OutputFile, ReapiExecClient,
};
use anyhow::{Context, Result, bail};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Instant;

pub struct RemoteWorker {
    pub worker_id: String,
    pub workspace_root: PathBuf,
    pub cas: LocalCas,
}

impl RemoteWorker {
    pub fn new(worker_id: String, workspace_root: PathBuf, cas: LocalCas) -> Result<Self> {
        fs::create_dir_all(&workspace_root)?;
        Ok(Self {
            worker_id,
            workspace_root,
            cas,
        })
    }

    /// Validates security constraints: prevents path traversal and dangerous absolute paths
    pub fn validate_path_security(path_str: &str) -> Result<()> {
        if path_str.contains("..") || path_str.starts_with('/') || path_str.starts_with('\\') {
            bail!(
                "Security Violation: Path traversal escape attempt in path '{}'",
                path_str
            );
        }
        Ok(())
    }

    pub fn execute_action(
        &self,
        action: &ActionSpec,
        command: &CommandSpec,
    ) -> Result<ActionResult> {
        let start_time = Instant::now();

        // 1. Path traversal security checks on outputs
        for file in &command.output_files {
            Self::validate_path_security(file)?;
        }
        for dir in &command.output_directories {
            Self::validate_path_security(dir)?;
        }

        // 2. Prepare isolated workspace directory for this action
        let action_dir = self
            .workspace_root
            .join(format!("work_{}", &action.command_digest.hash[7..15]));
        fs::create_dir_all(&action_dir)?;

        if command.arguments.is_empty() {
            bail!("Command arguments cannot be empty");
        }

        // 3. Execute command safely inside action directory
        let mut cmd = Command::new(&command.arguments[0]);
        if command.arguments.len() > 1 {
            cmd.args(&command.arguments[1..]);
        }
        cmd.current_dir(&action_dir);

        for (k, v) in &command.environment_variables {
            cmd.env(k, v);
        }

        let output = cmd
            .output()
            .with_context(|| format!("Remote worker failed executing {:?}", command.arguments))?;

        let mut output_files = Vec::new();

        // 4. Capture & store output files into CAS
        for target_rel in &command.output_files {
            let target_path = action_dir.join(target_rel);
            if target_path.exists() && target_path.is_file() {
                let content = fs::read(&target_path)?;
                let digest = ReapiExecClient::compute_digest(&content);
                self.cas.store_file(&digest.hash, &target_path)?;
                output_files.push(OutputFile {
                    path: target_rel.clone(),
                    digest,
                });
            }
        }

        // Clean up action directory
        let _ = fs::remove_dir_all(&action_dir);

        let duration_ms = start_time.elapsed().as_millis() as u64;

        Ok(ActionResult {
            exit_code: output.status.code().unwrap_or(-1),
            stdout_raw: output.stdout,
            stderr_raw: output.stderr,
            output_files,
            output_directories: Vec::new(),
            execution_duration_ms: duration_ms,
        })
    }
}