remem-ai 0.6.90

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};

use serde_json::Value;

const MAX_PUBLIC_ARTIFACT_BYTES: u64 = 64 * 1024 * 1024;

pub(super) fn read_bounded(path: &Path) -> Result<Vec<u8>, String> {
    let metadata = fs::metadata(path).map_err(|error| error.to_string())?;
    if metadata.len() > MAX_PUBLIC_ARTIFACT_BYTES {
        return Err("exceeds the 64 MiB public artifact limit".to_string());
    }
    fs::read(path).map_err(|error| error.to_string())
}

pub(super) fn private_payload_violation(bytes: &[u8]) -> Option<&'static str> {
    let Ok(text) = std::str::from_utf8(bytes) else {
        return Some("declared text artifact must be UTF-8");
    };
    if let Ok(value) = serde_json::from_str::<Value>(text) {
        return private_json_violation(&value);
    }
    private_string_violation(text)
}

fn private_json_violation(value: &Value) -> Option<&'static str> {
    match value {
        Value::String(text) => private_string_violation(text),
        Value::Array(items) => items.iter().find_map(private_json_violation),
        Value::Object(object) => object.values().find_map(private_json_violation),
        Value::Bool(_) | Value::Number(_) | Value::Null => None,
    }
}

pub(super) fn private_string_violation(text: &str) -> Option<&'static str> {
    if text.contains("~/.remem")
        || text.contains("$HOME/.remem")
        || text.contains("${HOME}/.remem")
        || (text.contains("/.remem/")
            && (text.contains("/Users/") || text.contains("/home/") || text.contains("/var/home/")))
    {
        return Some("contains a private remem path");
    }
    if let Some(home) = dirs::home_dir().and_then(|path| path.into_os_string().into_string().ok()) {
        if text.contains(&home) {
            return Some("contains an absolute user home path");
        }
    }
    contains_absolute_user_home_path(text).then_some("contains an absolute user home path")
}

fn contains_absolute_user_home_path(text: &str) -> bool {
    ["/Users/", "/home/", "/var/home/"].iter().any(|prefix| {
        text.match_indices(prefix).any(|(index, _)| {
            let suffix = &text[index + prefix.len()..];
            suffix
                .split_once('/')
                .is_some_and(|(user, path)| !user.is_empty() && !path.is_empty())
        })
    })
}

pub(super) fn unreferenced_run_entries(
    root: &Path,
    run_path: &Path,
    artifacts: &BTreeMap<String, String>,
) -> Vec<(String, String)> {
    let Some(parent) = run_path.parent() else {
        return vec![(
            display(root, run_path),
            "run artifact has no parent directory".to_string(),
        )];
    };
    let mut allowed = artifacts
        .values()
        .map(|path| root.join(path))
        .collect::<BTreeSet<PathBuf>>();
    allowed.insert(run_path.to_path_buf());
    let entries = match fs::read_dir(parent) {
        Ok(entries) => entries,
        Err(error) => {
            return vec![(
                display(root, parent),
                format!("read run artifact directory: {error}"),
            )];
        }
    };
    entries
        .filter_map(|entry| match entry {
            Ok(entry) if allowed.contains(&entry.path()) => None,
            Ok(entry) => Some((
                display(root, &entry.path()),
                "unreferenced file in v2 run artifact directory".to_string(),
            )),
            Err(error) => Some((
                display(root, parent),
                format!("read run artifact directory entry: {error}"),
            )),
        })
        .collect()
}

fn display(root: &Path, path: &Path) -> String {
    path.strip_prefix(root)
        .unwrap_or(path)
        .to_string_lossy()
        .replace('\\', "/")
}