openlatch-client 0.1.18

OpenLatch runtime enforcement node — the capture-and-enforce client for the AI Operations Platform
//! Hidden maintainer-only `__spike-update` subcommand — Phase 1 spike.
//!
//! Drives [`crate::update`]'s verifier + sanity-check + atomic-swap
//! primitives end-to-end against pre-built binaries supplied via CLI args.
//! No npm registry interaction, no tarball download, no daemon RPC — those
//! land in P2.
//!
//! See `.local/brainstorms/auto-update/PHASE-1-spike.md`.

use std::path::PathBuf;

use clap::Args;

use crate::update;

/// Arguments for the hidden `__spike-update` subcommand.
#[derive(Args, Debug, Clone)]
pub struct SpikeUpdateArgs {
    /// Path to the new openlatch[.exe] binary (already built; signed by us).
    #[arg(long)]
    pub exe: PathBuf,

    /// Path to the new openlatch-hook[.exe] binary.
    #[arg(long)]
    pub hook: PathBuf,

    /// Directory containing `openlatch.minisig` and `openlatch-hook.minisig`.
    #[arg(long)]
    pub sig_dir: PathBuf,

    /// Expected `--version` substring the new binary must emit before swap.
    #[arg(long)]
    pub expected_version: String,

    /// Override the resolved hook binary path (skips `locate_hook_binary`).
    /// Tests use this to point at an isolated install directory.
    #[arg(long, hide = true)]
    pub hook_install_path: Option<PathBuf>,

    /// After a successful swap, immediately call `restore_from_bak` to prove
    /// the rollback primitive works end-to-end.
    #[arg(long)]
    pub simulate_restart_loop: bool,
}

/// Run the spike subcommand. Returns `true` on success, `false` on any
/// failure (with stderr context already printed).
///
/// Output is plain stderr (`eprintln!`) since this is a maintainer tool
/// invoked outside the daemon's tracing context. Each significant step
/// prints a `[spike] ...` line so the test harness can pattern-match on
/// progress.
pub fn run(args: &SpikeUpdateArgs) -> bool {
    eprintln!("[spike] Phase 1 — verifying signatures…");

    let exe_sig = args.sig_dir.join("openlatch.minisig");
    if let Err(e) = update::verify_with_any_trusted_key(&args.exe, &exe_sig) {
        eprintln!("[spike] daemon signature verify failed: {e}");
        return false;
    }

    let hook_sig = args.sig_dir.join("openlatch-hook.minisig");
    if let Err(e) = update::verify_with_any_trusted_key(&args.hook, &hook_sig) {
        eprintln!("[spike] hook signature verify failed: {e}");
        return false;
    }
    eprintln!("[spike] signatures OK");

    eprintln!("[spike] sanity-checking staging binary…");
    if let Err(e) = update::sanity_check_version(&args.exe, &args.expected_version) {
        eprintln!("[spike] sanity check failed: {e}");
        return false;
    }
    eprintln!("[spike] sanity OK");

    eprintln!("[spike] resolving hook install path…");
    let hook_path = match args.hook_install_path.as_deref() {
        Some(p) => p.to_path_buf(),
        None => match update::locate_hook_binary() {
            Ok(p) => p,
            Err(e) => {
                eprintln!("[spike] hook resolution failed: {e}");
                return false;
            }
        },
    };
    eprintln!("[spike] hook resolved at {}", hook_path.display());

    eprintln!("[spike] performing swap…");
    let handle = match update::perform_swap(&args.exe, &args.hook, &hook_path) {
        Ok(h) => h,
        Err(e) => {
            eprintln!("[spike] swap failed: {e}");
            return false;
        }
    };
    eprintln!("[spike] swap complete");
    eprintln!("[spike]   current_exe = {}", handle.current_exe.display());
    eprintln!("[spike]   hook_path   = {}", handle.hook_path.display());
    eprintln!("[spike]   hook_bak    = {}", handle.hook_bak.display());

    if args.simulate_restart_loop {
        eprintln!("[spike] simulating rollback (restore_from_bak)…");
        if let Err(e) = update::restore_from_bak(&handle) {
            eprintln!("[spike] rollback failed: {e}");
            return false;
        }
        eprintln!("[spike] rollback complete (hook restored)");
        eprintln!(
            "[spike] note: daemon-side restore is implementation-defined \
             (self_replace leaves a `.<random>` sibling in the install \
             directory) and is left to P3's restart-loop rollback."
        );
    }

    eprintln!("[spike] DONE");
    true
}