vv-agent 0.6.2

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::path::{Path, PathBuf};

use super::SessionMemory;

const SESSION_MEMORY_FILENAME: &str = "session_memory.json";

impl SessionMemory {
    pub fn storage_path(&self) -> Option<PathBuf> {
        let workspace = self.workspace.as_ref()?;
        let workspace_root = workspace.canonicalize().ok()?;
        let storage_dir = if self.config.storage_dir.is_absolute() {
            self.config.storage_dir.clone()
        } else {
            workspace.join(&self.config.storage_dir)
        };
        let scoped_dir = match self
            .storage_scope
            .as_deref()
            .filter(|scope| !scope.is_empty())
        {
            Some(scope) => storage_dir.join(sanitize_storage_scope(scope)?),
            None => storage_dir,
        };
        let resolved = scoped_dir.join(SESSION_MEMORY_FILENAME);
        let parent = resolved.parent()?;
        let canonical_parent = if parent.exists() {
            parent.canonicalize().ok()?
        } else {
            canonicalize_existing_ancestor(parent)?
        };
        if !canonical_parent.starts_with(&workspace_root) {
            return None;
        }
        Some(resolved)
    }
}

fn sanitize_storage_scope(raw: &str) -> Option<String> {
    let safe = raw
        .trim()
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-') {
                ch
            } else {
                '_'
            }
        })
        .collect::<String>()
        .trim_matches(['.', '_', '-'])
        .to_string();
    (!safe.is_empty()).then_some(safe)
}

fn canonicalize_existing_ancestor(path: &Path) -> Option<PathBuf> {
    let mut current = path;
    while !current.exists() {
        current = current.parent()?;
    }
    current.canonicalize().ok()
}