//! devendra controller library.
//!
//! Owns the flat-file desired-state [`store`], the [`publish`] step that
//! authors it into an iroh document, and the [`web`] UI (axum + minijinja +
//! htmx, structured after ../dms). The controller is the single writer of
//! manifests and file bodies; agents write only their own status back.
use std::str::FromStr;
use std::sync::Arc;
use anyhow::{Context, Result};
use minijinja::Environment;
use devendra_node::{DocHandle, DocTicket, NamespaceId, Node, ShareMode};
pub mod adopt;
pub mod config;
pub mod publish;
pub mod store;
pub mod web;
pub use config::Config;
/// Shared state handed to every axum handler.
#[derive(Clone)]
pub struct AppState {
pub env: Environment<'static>,
pub node: Arc<Node>,
/// The document agents sync from. Cheap to clone (Arc-backed handle).
pub doc: DocHandle,
pub config: Arc<Config>,
}
/// Open the controller's document, reusing the persisted namespace across
/// restarts so the agent join ticket stays stable. Creates and records a new
/// namespace only when none is known yet.
pub async fn open_or_create_doc(node: &Node, config: &Config) -> Result<DocHandle> {
let ns_file = config.namespace_file();
if let Ok(recorded) = tokio::fs::read_to_string(&ns_file).await {
let id = NamespaceId::from_str(recorded.trim())
.with_context(|| format!("parsing recorded namespace in {}", ns_file.display()))?;
if let Some(doc) = node.open_doc(id).await? {
tracing::info!(doc = %id, "reopened existing controller document");
return Ok(doc);
}
tracing::warn!(
doc = %id,
"recorded namespace not found in store; creating a fresh document"
);
}
let doc = node.create_doc().await?;
tokio::fs::create_dir_all(&config.data_dir)
.await
.context("creating data dir")?;
tokio::fs::write(&ns_file, doc.id().to_string())
.await
.with_context(|| format!("recording namespace to {}", ns_file.display()))?;
tracing::info!(doc = %doc.id(), "created new controller document");
Ok(doc)
}
/// Produce a write-capable join ticket agents use to sync. Write capability
/// lets them post their status back into the document.
///
/// The ticket is generated fresh each time (never cached) so it always carries
/// the endpoint's *current* network addresses. Stability across restarts comes
/// from the persisted secret key (stable node id) and the controller's fixed
/// iroh port (stable address) — not from freezing the ticket string, which
/// would go stale if the address ever changed.
pub async fn agent_ticket(node: &Node, doc: &DocHandle) -> Result<DocTicket> {
node.share(doc, ShareMode::Write).await
}
neiam
/devendra
public · Issues · Pulls · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
2.7 KiB · text
History
d69022d