Skip to main content

oxi/
services.rs

1//! Composition root for oxi-cli.
2//!
3//! Wires concrete file-based port implementations (from `oxi-fs`) to the
4//! `Oxi` engine. Future run modes (TUI / print / RPC) build on top of
5//! the `Oxi` produced here.
6//!
7//! # Migration note
8//!
9//! The legacy `App` struct in `lib.rs` is the **single-user interactive**
10//! composition (TUI-driven, in-process). This module is the
11//! **port-based** composition: a `Oxi` engine with persistence, auth,
12//! config, and skills wired via `oxi_sdk::OxiBuilder::with_port_*`.
13//!
14//! Both paths are expected to coexist during the migration. New run modes
15//! should consume `build_oxi(...)` from this module; the legacy `App`
16//! path remains for the interactive TUI until it is fully migrated.
17
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20
21use anyhow::{Context, Result};
22
23use oxi_sdk::fs::{
24    FileAuthProvider, FileConfigStore, FilePersonaProvider, FileSkillLoader, FileStateStore,
25    SimpleAccessGate, TomlCapabilityResolver,
26};
27use oxi_sdk::inmem::{
28    CountingResourceMonitor, InMemoryCronScheduler, InMemoryMemoryStore, InProcessEventBus,
29};
30use oxi_sdk::Oxi;
31
32/// Resolved paths under the oxi home directory.
33#[derive(Debug, Clone)]
34pub struct OxiPaths {
35    /// Root directory (`$OXI_HOME` or `$HOME/.oxi`).
36    pub home: PathBuf,
37    /// `auth.json` location.
38    pub auth: PathBuf,
39    /// `settings.toml` location.
40    pub config: PathBuf,
41    /// Sessions directory.
42    pub sessions: PathBuf,
43    /// Skills root.
44    pub skills: PathBuf,
45}
46
47impl OxiPaths {
48    /// Resolve from the conventional home directory.
49    pub fn from_home(home: impl Into<PathBuf>) -> Self {
50        let home = home.into();
51        Self {
52            auth: home.join("auth.json"),
53            config: home.join("settings.toml"),
54            sessions: home.join("sessions"),
55            skills: home.join("skills"),
56            home,
57        }
58    }
59
60    /// Default — uses `$OXI_HOME` or `$HOME/.oxi`.
61    pub fn default_paths() -> Result<Self> {
62        oxi_sdk::fs::home_dir()
63            .map(Self::from_home)
64            .context("could not resolve oxi home directory")
65    }
66}
67
68/// Build an `Oxi` engine wired with file-based port implementations.
69///
70/// This is the **composition root** for oxi-cli. It is intentionally
71/// side-effect-light: it does not touch the network or start any task.
72/// Run modes (TUI, print, RPC) take the returned `Oxi` and run.
73pub fn build_oxi(paths: &OxiPaths) -> Result<Oxi> {
74    ensure_parent(&paths.auth)?;
75    ensure_parent(&paths.config)?;
76    ensure_parent(&paths.sessions)?;
77
78    let oxi = oxi_sdk::OxiBuilder::new()
79        .with_builtins()
80        .with_state(Arc::new(FileStateStore::new(&paths.sessions)))
81        .with_auth(Arc::new(FileAuthProvider::new(&paths.auth)))
82        .with_config(Arc::new(FileConfigStore::new(&paths.config)))
83        .with_skills(Arc::new(FileSkillLoader::single(&paths.skills)))
84        .with_personas(Arc::new(FilePersonaProvider::new(paths.home.join("personas"))))
85        .with_access(Arc::new(SimpleAccessGate::from_file(
86            paths.home.join("access.toml"),
87        )))
88        .with_capabilities(Arc::new(TomlCapabilityResolver::from_file(
89            paths.home.join("capabilities.toml"),
90        )))
91        .with_event_bus(InProcessEventBus::new(64))
92        .with_memory(Arc::new(InMemoryMemoryStore::new()))
93        .with_cron(Arc::new(InMemoryCronScheduler::new()))
94        .with_resources(Arc::new(CountingResourceMonitor::new()))
95        .build();
96
97    Ok(oxi)
98}
99
100fn ensure_parent(path: &Path) -> Result<()> {
101    if let Some(parent) = path.parent() {
102        std::fs::create_dir_all(parent)
103            .with_context(|| format!("create_dir_all {}", parent.display()))?;
104    }
105    Ok(())
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn paths_are_consistent() {
114        let p = OxiPaths::from_home("/tmp/oxi-test");
115        assert!(p.auth.starts_with("/tmp/oxi-test"));
116        assert!(p.config.starts_with("/tmp/oxi-test"));
117        assert!(p.sessions.starts_with("/tmp/oxi-test"));
118        assert!(p.skills.starts_with("/tmp/oxi-test"));
119    }
120
121    #[test]
122    fn build_oxi_succeeds() {
123        let tmp = tempfile::TempDir::new().unwrap();
124        let paths = OxiPaths::from_home(tmp.path());
125        let oxi = build_oxi(&paths).unwrap();
126        // State is wired (even if we don't call it).
127        let _ = oxi.ports().state;
128    }
129}