holon 0.14.1

A headless, event-driven runtime for long-lived agents
Documentation
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, RwLock as StdRwLock};

use anyhow::{anyhow, Result};

use crate::{
    config::AppConfig,
    storage::AppStorage,
    system::WorkspaceAccessMode,
    types::{AgentIdentityRecord, WorkspaceEntry, WorkspaceOccupancyRecord},
};

#[derive(Clone)]
pub(crate) struct RuntimeRegistry {
    inner: Arc<RuntimeRegistryInner>,
}

struct RuntimeRegistryInner {
    config: AppConfig,
    host_storage: AppStorage,
    agent_identities: StdRwLock<HashMap<String, AgentIdentityRecord>>,
}

impl RuntimeRegistry {
    pub(crate) fn new(config: AppConfig) -> Result<Self> {
        let host_storage = AppStorage::new(config.home_dir.join("host"))?;
        let agent_identities = host_storage
            .latest_agent_identities()?
            .into_iter()
            .map(|record| (record.agent_id.clone(), record))
            .collect();
        Ok(Self {
            inner: Arc::new(RuntimeRegistryInner {
                config,
                host_storage,
                agent_identities: StdRwLock::new(agent_identities),
            }),
        })
    }

    pub(crate) fn config(&self) -> &AppConfig {
        &self.inner.config
    }

    pub(crate) fn agent_identity_record(
        &self,
        agent_id: &str,
    ) -> Result<Option<AgentIdentityRecord>> {
        Ok(self
            .inner
            .agent_identities
            .read()
            .expect("agent identities cache poisoned")
            .get(agent_id)
            .cloned())
    }

    pub(crate) fn agent_identity_records(&self) -> Result<Vec<AgentIdentityRecord>> {
        let mut records = self
            .inner
            .agent_identities
            .read()
            .expect("agent identities cache poisoned")
            .values()
            .cloned()
            .collect::<Vec<_>>();
        records.sort_by(|left, right| left.agent_id.cmp(&right.agent_id));
        Ok(records)
    }

    pub(crate) fn append_agent_identity(&self, record: &AgentIdentityRecord) -> Result<()> {
        self.inner.host_storage.append_agent_identity(record)?;
        self.inner
            .agent_identities
            .write()
            .expect("agent identities cache poisoned")
            .insert(record.agent_id.clone(), record.clone());
        Ok(())
    }

    pub(crate) fn workspace_entries(&self) -> Result<Vec<WorkspaceEntry>> {
        self.inner.host_storage.latest_workspace_entries()
    }

    pub(crate) fn workspace_occupancies(&self) -> Result<Vec<WorkspaceOccupancyRecord>> {
        self.inner.host_storage.latest_workspace_occupancies()
    }

    pub(crate) fn append_workspace_occupancy(
        &self,
        record: &WorkspaceOccupancyRecord,
    ) -> Result<()> {
        self.inner.host_storage.append_workspace_occupancy(record)
    }

    pub(crate) fn workspace_occupancy_by_id(
        &self,
        occupancy_id: &str,
    ) -> Result<Option<WorkspaceOccupancyRecord>> {
        Ok(self
            .workspace_occupancies()?
            .into_iter()
            .find(|record| record.occupancy_id == occupancy_id))
    }

    pub(crate) fn active_workspace_occupancies_for_root(
        &self,
        execution_root_id: &str,
    ) -> Result<Vec<WorkspaceOccupancyRecord>> {
        Ok(self
            .workspace_occupancies()?
            .into_iter()
            .filter(|record| {
                record.execution_root_id == execution_root_id && record.released_at.is_none()
            })
            .collect())
    }

    pub(crate) fn acquire_workspace_occupancy(
        &self,
        workspace_id: &str,
        execution_root_id: &str,
        holder_agent_id: &str,
        access_mode: WorkspaceAccessMode,
    ) -> Result<Option<WorkspaceOccupancyRecord>> {
        let active = self.active_workspace_occupancies_for_root(execution_root_id)?;
        if let Some(existing) = active.iter().find(|record| {
            record.holder_agent_id == holder_agent_id && record.access_mode == access_mode
        }) {
            return Ok(Some(existing.clone()));
        }
        if access_mode == WorkspaceAccessMode::ExclusiveWrite
            && active.iter().any(|record| {
                record.holder_agent_id != holder_agent_id
                    && record.access_mode == WorkspaceAccessMode::ExclusiveWrite
            })
        {
            return Err(anyhow!(
                "execution root {} already has an exclusive_write holder",
                execution_root_id
            ));
        }
        let record = WorkspaceOccupancyRecord {
            occupancy_id: format!("occ-{}", uuid::Uuid::new_v4().simple()),
            execution_root_id: execution_root_id.to_string(),
            workspace_id: workspace_id.to_string(),
            holder_agent_id: holder_agent_id.to_string(),
            access_mode,
            acquired_at: chrono::Utc::now(),
            released_at: None,
        };
        self.append_workspace_occupancy(&record)?;
        Ok(Some(record))
    }

    pub(crate) fn release_workspace_occupancy(
        &self,
        occupancy_id: &str,
    ) -> Result<Option<WorkspaceOccupancyRecord>> {
        let Some(mut record) = self.workspace_occupancy_by_id(occupancy_id)? else {
            return Ok(None);
        };
        if record.released_at.is_some() {
            return Ok(Some(record));
        }
        record.released_at = Some(chrono::Utc::now());
        self.append_workspace_occupancy(&record)?;
        Ok(Some(record))
    }

    pub(crate) fn ensure_workspace_entry(
        &self,
        workspace_anchor: PathBuf,
    ) -> Result<WorkspaceEntry> {
        let workspace_anchor = crate::system::workspace::normalize_path(&workspace_anchor)?;
        if let Some(existing) = self
            .inner
            .host_storage
            .latest_workspace_entries()?
            .into_iter()
            .find(|entry| entry.workspace_anchor == workspace_anchor)
        {
            return Ok(existing);
        }

        let repo_name = workspace_anchor
            .file_name()
            .and_then(|name| name.to_str())
            .map(ToString::to_string);
        let entry = WorkspaceEntry::new(
            format!("ws-{}", uuid::Uuid::new_v4().simple()),
            workspace_anchor,
            repo_name,
        );
        self.inner.host_storage.append_workspace_entry(&entry)?;
        Ok(entry)
    }

    pub(crate) fn ensure_default_agent_identity(&self) -> Result<AgentIdentityRecord> {
        self.validate_agent_id(&self.inner.config.default_agent_id)?;
        if let Some(existing) = self.agent_identity_record(&self.inner.config.default_agent_id)? {
            return Ok(existing);
        }
        let record = AgentIdentityRecord::new(
            self.inner.config.default_agent_id.clone(),
            crate::types::AgentKind::Default,
            crate::types::AgentVisibility::Public,
            crate::types::AgentOwnership::SelfOwned,
            crate::types::AgentProfilePreset::PublicNamed,
            None,
            None,
        );
        self.append_agent_identity(&record)?;
        Ok(record)
    }

    pub(crate) fn validate_agent_id(&self, agent_id: &str) -> Result<()> {
        validate_agent_id_format(agent_id)
    }
}

pub(crate) fn validate_agent_id_format(agent_id: &str) -> Result<()> {
    use std::path::{Component, Path};

    if agent_id.is_empty() {
        return Err(anyhow!("agent id must not be empty"));
    }
    if !agent_id
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
    {
        return Err(anyhow!(
            "agent id must contain only ASCII letters, digits, '.', '-', or '_'"
        ));
    }

    let mut components = Path::new(agent_id).components();
    let valid_component =
        matches!(components.next(), Some(Component::Normal(_))) && components.next().is_none();
    if !valid_component {
        return Err(anyhow!("agent id must be a single normal path component"));
    }
    Ok(())
}