openlatch-client 0.6.3

OpenLatch runtime enforcement node — the capture-and-enforce adapter that evaluates every covered action against a coding agent's Autonomy Zone before it runs
//! Windows: `HKCU\Environment` (REG_SZ), then a `WM_SETTINGCHANGE` broadcast so Explorer picks the change up for the
//! apps it launches next.

use std::ffi::{OsStr, OsString};
use std::os::windows::ffi::{OsStrExt, OsStringExt};

use windows_sys::Win32::Foundation::{ERROR_FILE_NOT_FOUND, ERROR_SUCCESS};
use windows_sys::Win32::System::Registry::{
    RegDeleteKeyValueW, RegGetValueW, RegSetKeyValueW, HKEY_CURRENT_USER, REG_SZ, RRF_NOEXPAND,
    RRF_RT_REG_EXPAND_SZ, RRF_RT_REG_SZ,
};
use windows_sys::Win32::UI::WindowsAndMessaging::{
    SendMessageTimeoutW, HWND_BROADCAST, SMTO_ABORTIFHUNG, WM_SETTINGCHANGE,
};

pub(super) const MANUAL_CLEAR: &str =
    "`reg delete HKCU\\Environment /v CLINE_WRAPPER_PATH /f` and \
     `reg delete HKCU\\Environment /v CLINE_JS_RUNTIME_PATH /f`";

const SUBKEY: &str = "Environment";
/// A value larger than this is not a path.
const MAX_VALUE_BYTES: u32 = 64 * 1024;
/// The broadcast's budget, in milliseconds (the module's 5 s).
const BROADCAST_MS: u32 = 5000;

fn wide(s: &str) -> Vec<u16> {
    OsStr::new(s)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect()
}

/// Tells running top-level windows (Explorer first) that the user environment changed. Best effort: its answer
/// is the broadcast's return code, reported as an error when zero.
fn broadcast() -> Result<(), String> {
    let param = wide(SUBKEY);
    let mut result: usize = 0;
    // SAFETY: `param` is a NUL-terminated UTF-16 buffer alive for the whole call (the call returns only after
    // every window answered or the timeout passed), and `result` is a live out-parameter.
    let rc = unsafe {
        SendMessageTimeoutW(
            HWND_BROADCAST,
            WM_SETTINGCHANGE,
            0,
            param.as_ptr() as isize,
            SMTO_ABORTIFHUNG,
            BROADCAST_MS,
            &mut result,
        )
    };
    if rc == 0 {
        Err("the WM_SETTINGCHANGE broadcast did not complete".to_string())
    } else {
        Ok(())
    }
}

pub(super) fn read(name: &str) -> Result<Option<String>, String> {
    let subkey = wide(SUBKEY);
    let value = wide(name);
    let flags = RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ | RRF_NOEXPAND;
    let mut bytes: u32 = 0;
    // SAFETY: `subkey` and `value` are NUL-terminated UTF-16 buffers alive for the call; a null data pointer
    // with a live size out-parameter is the documented sizing form.
    let rc = unsafe {
        RegGetValueW(
            HKEY_CURRENT_USER,
            subkey.as_ptr(),
            value.as_ptr(),
            flags,
            std::ptr::null_mut(),
            std::ptr::null_mut(),
            &mut bytes,
        )
    };
    if rc == ERROR_FILE_NOT_FOUND {
        return Ok(None);
    }
    if rc != ERROR_SUCCESS {
        return Err(format!("RegGetValueW({name}) failed: {rc}"));
    }
    if bytes > MAX_VALUE_BYTES {
        return Err(format!("{name} is larger than {MAX_VALUE_BYTES} bytes"));
    }
    let mut buf = vec![0u16; bytes as usize / 2 + 1];
    let mut written = bytes;
    // SAFETY: `buf` holds at least `written` bytes of writable UTF-16 storage and `written` is a live u32
    // out-parameter.
    let rc = unsafe {
        RegGetValueW(
            HKEY_CURRENT_USER,
            subkey.as_ptr(),
            value.as_ptr(),
            flags,
            std::ptr::null_mut(),
            buf.as_mut_ptr().cast(),
            &mut written,
        )
    };
    if rc == ERROR_FILE_NOT_FOUND {
        return Ok(None);
    }
    if rc != ERROR_SUCCESS {
        return Err(format!("RegGetValueW({name}) failed: {rc}"));
    }
    let len = (written as usize / 2).saturating_sub(1).min(buf.len());
    let text = OsString::from_wide(&buf[..len])
        .to_string_lossy()
        .into_owned();
    Ok((!text.is_empty()).then_some(text))
}

pub(super) fn apply(name: &str, value: &str) -> Result<(), String> {
    let subkey = wide(SUBKEY);
    let key = wide(name);
    let data = wide(value);
    let len = u32::try_from(data.len() * 2).map_err(|_| format!("{name}: value too long"))?;
    // SAFETY: every pointer names a NUL-terminated UTF-16 buffer alive for the call; `len` is `data`'s size in
    // bytes, terminator included, as REG_SZ requires.
    let rc = unsafe {
        RegSetKeyValueW(
            HKEY_CURRENT_USER,
            subkey.as_ptr(),
            key.as_ptr(),
            REG_SZ,
            data.as_ptr().cast(),
            len,
        )
    };
    if rc != ERROR_SUCCESS {
        return Err(format!("RegSetKeyValueW({name}) failed: {rc}"));
    }
    if let Err(e) = broadcast() {
        tracing::debug!(error = %e, "Cline login environment: broadcast after a write");
    }
    Ok(())
}

pub(super) fn clear(name: &str) -> Result<(), String> {
    let subkey = wide(SUBKEY);
    let key = wide(name);
    // SAFETY: both pointers name NUL-terminated UTF-16 buffers alive for the call.
    let rc = unsafe { RegDeleteKeyValueW(HKEY_CURRENT_USER, subkey.as_ptr(), key.as_ptr()) };
    if rc != ERROR_SUCCESS && rc != ERROR_FILE_NOT_FOUND {
        return Err(format!("RegDeleteKeyValueW({name}) failed: {rc}"));
    }
    if let Err(e) = broadcast() {
        tracing::debug!(error = %e, "Cline login environment: broadcast after a removal");
    }
    Ok(())
}