use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use anyhow::{Context, Result};
use oxicode_sdk::Oxicode;
use oxicode_sdk::fs::{
FileConfigStore, FileModelCatalog, FilePersonaProvider, FileSkillLoader, FileStateStore,
SimpleAccessGate, TomlCapabilityResolver,
};
use oxicode_sdk::inmem::{
CountingResourceMonitor, InMemoryCronScheduler, InMemoryMemoryStore, InProcessEventBus,
};
use oxicode_sdk::ports::InternalUrlRouter;
use oxicode_sdk::ports::catalog::CatalogEvent;
use oxicode_sdk::ports::fs::CatalogConfig;
use oxicode_sdk::ports::inmem::url_router::CompositeUrlRouter;
use crate::internal_urls::issue_handler::IssueProtocolHandler;
use crate::internal_urls::memory_handler::MemoryProtocolHandler;
use crate::internal_urls::pr_handler::PrProtocolHandler;
#[derive(Debug, Clone)]
pub struct OxicodePaths {
pub home: PathBuf,
pub auth: PathBuf,
pub config: PathBuf,
pub sessions: PathBuf,
pub skills: PathBuf,
pub foundation: Option<PathBuf>,
}
impl OxicodePaths {
pub fn from_home(home: impl Into<PathBuf>) -> Self {
let home = home.into();
Self {
auth: home.join("auth.json"),
config: home.join("settings.toml"),
sessions: home.join("sessions"),
skills: home.join("skills"),
home,
foundation: crate::foundation::foundation_root(),
}
}
pub fn default_paths() -> Result<Self> {
oxicode_sdk::fs::home_dir()
.map(Self::from_home)
.context("could not resolve oxicode home directory")
}
}
pub async fn build_oxicode(
paths: &OxicodePaths,
embedding_provider: Option<Arc<dyn oxicode_sdk::ports::EmbeddingProvider>>,
hook_runner: Option<Arc<dyn oxicode_sdk::ports::HookRunner>>,
) -> Result<Oxicode> {
build_oxicode_with_catalog(
paths,
build_catalog_config(paths),
embedding_provider,
hook_runner,
)
.await
}
pub async fn build_oxicode_with_catalog(
paths: &OxicodePaths,
catalog_config: CatalogConfig,
embedding_provider: Option<Arc<dyn oxicode_sdk::ports::EmbeddingProvider>>,
hook_runner: Option<Arc<dyn oxicode_sdk::ports::HookRunner>>,
) -> Result<Oxicode> {
ensure_parent(&paths.auth)?;
ensure_parent(&paths.config)?;
ensure_parent(&paths.sessions)?;
let foundation_provider: Option<Arc<dyn oxicode_ai::Provider>> = if let Some(froot) = paths
.foundation
.clone()
.or_else(crate::foundation::foundation_root)
{
if crate::foundation::foundation_present(&froot) {
match resolve_and_register_profile(&froot).await {
Ok(p) => Some(p),
Err(e) => {
tracing::warn!(
"Foundation v1 profile resolution failed: {e}; \
engine will start without a registered provider"
);
None
}
}
} else {
None
}
} else {
None
};
let catalog: Arc<dyn oxicode_sdk::ports::catalog::ModelCatalog> =
match FileModelCatalog::init(catalog_config).await {
Ok(c) => c,
Err(e) => {
tracing::warn!(error = %e, "catalog init failed; continuing with noop");
oxicode_sdk::NoopModelCatalog::new()
}
};
let skill_loader = Arc::new(FileSkillLoader::single(&paths.skills));
let rule_registry: Arc<dyn oxicode_sdk::ports::RuleRegistry> =
Arc::new(oxicode_sdk::ports::NoopRuleRegistry);
let agent_artifact_store = crate::internal_urls::agent_handler::AgentArtifactStore::new();
let local_root = paths.home.join("local-artifacts");
let mut builder = oxicode_sdk::OxicodeBuilder::new()
.with_builtins()
.with_state(Arc::new(FileStateStore::new(&paths.sessions)))
.with_auth(crate::store::auth_storage::shared_auth_storage())
.with_config(Arc::new(FileConfigStore::new(&paths.config)))
.with_skills(skill_loader.clone())
.with_personas(Arc::new(FilePersonaProvider::new(
paths.home.join("personas"),
)))
.with_access(Arc::new(SimpleAccessGate::from_file(
paths.home.join("access.toml"),
)))
.with_capabilities(Arc::new(TomlCapabilityResolver::from_file(
paths.home.join("capabilities.toml"),
)))
.with_event_bus(InProcessEventBus::new(64))
.with_memory(Arc::new(InMemoryMemoryStore::new()))
.with_cron(Arc::new(InMemoryCronScheduler::new()))
.with_resources(Arc::new(CountingResourceMonitor::new()))
.with_catalog(catalog)
.with_url_router(build_url_router(
paths,
skill_loader,
rule_registry,
agent_artifact_store,
local_root,
));
if let Some(ep) = embedding_provider {
builder = builder.with_embeddings(ep);
}
if let Some(runner) = hook_runner {
builder = builder.with_hooks(runner.clone());
}
if let Some(provider) = foundation_provider {
builder = builder.provider_arc("<foundation>", provider);
}
let oxicode = builder.build();
Ok(oxicode)
}
fn build_url_router(
paths: &OxicodePaths,
skill_loader: Arc<dyn oxicode_sdk::ports::SkillLoader>,
rule_registry: Arc<dyn oxicode_sdk::ports::RuleRegistry>,
agent_store: crate::internal_urls::agent_handler::AgentArtifactStore,
local_root: PathBuf,
) -> Arc<dyn InternalUrlRouter> {
let memory_root = paths.home.join("memory");
let router = CompositeUrlRouter::new();
let handler: Arc<dyn oxicode_sdk::ports::ProtocolHandler> =
if crate::foundation::foundation_present(
&crate::foundation::foundation_root()
.unwrap_or_else(|| std::path::PathBuf::from("~/.oxi/foundation/v1")),
) {
let socket = crate::foundation::brain::default_socket_path();
let brain = Arc::new(crate::foundation::brain::BrainMemoryBackend::new(socket));
Arc::new(MemoryProtocolHandler::new(brain))
} else {
struct LegacyHandler {
memory_root: PathBuf,
}
#[async_trait::async_trait]
impl oxicode_sdk::ports::ProtocolHandler for LegacyHandler {
fn scheme(&self) -> &str {
"memory"
}
async fn resolve(
&self,
url: &str,
_selector: Option<&str>,
_ctx: &oxicode_sdk::ports::ResolveContext,
) -> Result<oxicode_sdk::ports::ResolvedUrl, oxicode_sdk::SdkError>
{
let content = crate::internal_urls::memory_handler::resolve_memory_url_legacy(
url,
&self.memory_root,
)
.ok_or_else(|| oxicode_sdk::SdkError::PortNotConfigured { port: "memory" })?;
let size = content.len();
Ok(oxicode_sdk::ports::ResolvedUrl {
url: url.to_string(),
content,
content_type: "text/markdown".to_string(),
size: Some(size),
source_path: None,
notes: vec![],
immutable: true,
})
}
}
Arc::new(LegacyHandler { memory_root })
};
router.register(handler);
router.register(Arc::new(IssueProtocolHandler));
router.register(Arc::new(PrProtocolHandler));
router.register(Arc::new(
crate::internal_urls::skill_handler::SkillProtocolHandler::new(skill_loader),
));
router.register(Arc::new(
crate::internal_urls::rule_handler::RuleProtocolHandler::new(rule_registry),
));
router.register(Arc::new(
crate::internal_urls::agent_handler::AgentProtocolHandler::new(agent_store),
));
router.register(Arc::new(
crate::internal_urls::local_handler::LocalProtocolHandler::new(local_root),
));
Arc::new(router)
}
fn build_catalog_config(paths: &OxicodePaths) -> CatalogConfig {
CatalogConfig {
cache_path: paths.home.join("cache").join("models-dev.json"),
etag_path: paths.home.join("cache").join("models-dev.json.etag"),
override_path: paths.home.join("catalog").join("overrides.toml"),
mtime_window: std::time::Duration::from_secs(60 * 60),
fetch_enabled: std::env::var("OXICODE_MODELS_DEV_DISABLE_FETCH")
.ok()
.map(|v| !matches!(v.as_str(), "1" | "true" | "TRUE"))
.unwrap_or(true),
models_dev_url: std::env::var("OXICODE_MODELS_DEV_URL")
.unwrap_or_else(|_| "https://models.dev".to_string()),
user_agent: format!("oxicode-cli/{}", env!("CARGO_PKG_VERSION")),
local_discovery_urls: local_discovery_from_env(),
snapshot_path: paths.home.join("cache").join("models-dev.json"),
}
}
fn local_discovery_from_env() -> Vec<String> {
std::env::var("OXICODE_LOCAL_DISCOVERY")
.ok()
.map(|s| {
s.split(',')
.map(|u| u.trim().to_string())
.filter(|u| !u.is_empty())
.collect()
})
.unwrap_or_default()
}
pub fn spawn_catalog_event_logger(
catalog: Arc<dyn oxicode_sdk::ports::catalog::ModelCatalog>,
) -> tokio::task::JoinHandle<()> {
let mut rx = catalog.subscribe();
tokio::spawn(async move {
while let Ok(event) = rx.recv().await {
match event {
CatalogEvent::Updated {
provider_count,
model_count,
} => {
tracing::info!(provider_count, model_count, "catalog refreshed");
}
CatalogEvent::RefreshFailed { reason, .. } => {
tracing::warn!(reason, "catalog refresh failed");
}
CatalogEvent::OverrideApplied {
path,
provider_overrides,
model_overrides,
} => {
tracing::info!(
path = %path.display(),
provider_overrides,
model_overrides,
"catalog overrides applied"
);
}
CatalogEvent::LocalDiscovered {
base_url,
model_count,
} => {
tracing::info!(base_url, model_count, "local models discovered");
}
}
}
})
}
fn ensure_parent(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("create_dir_all {}", parent.display()))?;
}
Ok(())
}
pub fn build_embedding_provider(
settings: &crate::store::settings::Settings,
) -> Option<Arc<dyn oxicode_mnemopi::EmbeddingProvider>> {
match settings.embedding_provider.as_str() {
"remote" => build_remote_embedding_provider(settings),
_ => None,
}
}
fn build_remote_embedding_provider(
settings: &crate::store::settings::Settings,
) -> Option<Arc<dyn oxicode_mnemopi::EmbeddingProvider>> {
let base_url = settings.embedding_base_url.as_deref()?.trim();
if base_url.is_empty() {
tracing::warn!("memory: embedding_provider='remote' but embedding_base_url is empty");
return None;
}
let api_key = std::env::var(&settings.embedding_api_key_env).ok()?;
if api_key.is_empty() {
tracing::warn!(
"memory: embedding_provider='remote' but env var {} is unset",
settings.embedding_api_key_env
);
return None;
}
let model = if settings.embedding_model.is_empty() {
"text-embedding-3-small".to_string()
} else {
settings.embedding_model.clone()
};
Some(Arc::new(oxicode_mnemopi::RemoteEmbeddingProvider::new(
base_url, &api_key, &model,
)))
}
pub struct MnemopiEmbeddingBridge {
inner: Arc<dyn oxicode_mnemopi::EmbeddingProvider>,
}
impl MnemopiEmbeddingBridge {
pub fn new(inner: Arc<dyn oxicode_mnemopi::EmbeddingProvider>) -> Self {
Self { inner }
}
}
impl oxicode_sdk::ports::EmbeddingProvider for MnemopiEmbeddingBridge {
fn embed<'a>(
&'a self,
text: &'a str,
) -> Pin<Box<dyn Future<Output = Result<Vec<f32>, oxicode_sdk::SdkError>> + Send + 'a>> {
Box::pin(async move {
let inner = Arc::clone(&self.inner);
let text = text.to_string();
let result = tokio::task::spawn_blocking(move || inner.embed(&[text]))
.await
.map_err(|e| {
oxicode_sdk::SdkError::Internal(anyhow::anyhow!("embedding task panicked: {e}"))
})?;
let mut vectors = result.map_err(|e| {
oxicode_sdk::SdkError::Internal(anyhow::anyhow!("embedding failed: {e}"))
})?;
vectors.pop().ok_or_else(|| {
oxicode_sdk::SdkError::Internal(anyhow::anyhow!("embedding returned no vectors"))
})
})
}
}
pub fn create_memory_backend(
settings: &crate::store::settings::Settings,
) -> Option<Arc<dyn oxicode_agent::tools::MemoryBackend>> {
if !settings.memory_enabled {
return None;
}
let foundation_root = crate::foundation::foundation_root()
.unwrap_or_else(|| std::path::PathBuf::from("~/.oxi/foundation/v1"));
if crate::foundation::foundation_present(&foundation_root) {
let socket = crate::foundation::brain::default_socket_path();
let backend = crate::foundation::brain::BrainMemoryBackend::new(socket);
tracing::info!(
"Foundation v1 host active: durable memory authority is oxibrain \
(health: {})",
backend.health().info()
);
return Some(Arc::new(backend));
}
tracing::warn!(
"Foundation v1 host: oxibrain daemon unavailable; durable-memory \
tools will return typed unavailable results. Run `oxicode setup` to \
initialize the Foundation installation, or start the oxibrain daemon."
);
None
}
#[cfg(test)]
mod memory_backend_tests {
use super::*;
#[test]
fn brain_backend_returned_when_foundation_present() {
let tmp = tempdir_fixture();
unsafe {
std::env::set_var("OXI_FOUNDATION_HOME", &tmp);
}
std::fs::write(
tmp.join("foundation.json"),
r#"{"schema_version":1,"foundation":{"hosts":{"oxicode":">=0.1.0"}}}"#,
)
.unwrap();
std::fs::write(
tmp.join("profiles.json"),
r#"{"schema_version":1,"profiles":[]}"#,
)
.unwrap();
let backend = create_memory_backend(&test_settings());
assert!(backend.is_some(), "foundation fixture ⇒ brain backend");
unsafe {
std::env::remove_var("OXI_FOUNDATION_HOME");
}
}
#[test]
fn absent_foundation_returns_none() {
unsafe {
std::env::set_var("OXI_FOUNDATION_HOME", "/tmp/does-not-exist-foundation");
}
let backend = create_memory_backend(&test_settings());
assert!(
backend.is_none(),
"absent foundation ⇒ no local durable fallback (plan §5.h)"
);
unsafe {
std::env::remove_var("OXI_FOUNDATION_HOME");
}
}
fn test_settings() -> crate::store::settings::Settings {
let mut s = crate::store::settings::Settings::default();
s.memory_enabled = true;
s
}
fn tempdir_fixture() -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"oxicode-services-test-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
}
pub async fn build_memory_recall(
backend: &dyn oxicode_agent::tools::MemoryBackend,
subject: &str,
) -> String {
match backend.list(subject).await {
Ok(items) if !items.is_empty() => {
let mut block = String::from(
"\n\n## Project Memory\n\nThe following facts were learned in previous sessions:\n",
);
for item in &items {
block.push_str(&format!("- [{}] {}\n", item.kind, item.content));
}
block
}
_ => String::new(),
}
}
pub async fn session_reflect(
backend: &dyn oxicode_agent::tools::MemoryBackend,
subject: &str,
summary: &str,
) {
if let Err(e) = backend.put(summary, "summary", subject).await {
tracing::warn!("Failed to store session memory: {e}");
}
}
pub fn start_memory_pipeline(
_settings: &crate::store::settings::Settings,
_cwd: &Path,
_oxicode: Option<&oxicode_sdk::Oxicode>,
) -> Option<tokio::task::JoinHandle<()>> {
None
}
async fn resolve_and_register_profile(
foundation_root: &Path,
) -> Result<Arc<dyn oxicode_ai::Provider>, crate::foundation::FoundationError> {
use crate::foundation::profiles::{
EnvironmentOverride, ResolveInput, read as read_profiles, resolve_profile,
};
let profiles_path = foundation_root.join(crate::foundation::files::PROFILES);
let profiles = read_profiles(&profiles_path)?;
let explicit_profile = std::env::var("OXICODE_PROFILE")
.ok()
.filter(|s| !s.trim().is_empty());
let compat_import_path = foundation_root.join("compatibility.json");
let compat_import =
crate::foundation::compat_import::read_compatibility_shim(&compat_import_path)?;
let env_override = EnvironmentOverride::from_env();
let resolved = resolve_profile(ResolveInput {
explicit_profile: explicit_profile.as_deref(),
explicit_environment_override: env_override.as_ref(),
requested_role: None,
foundation_profiles: &profiles,
compatibility_import: compat_import.as_ref(),
})?;
let resolver = crate::foundation::credentials::KeychainCredentialResolver::default();
let credential = resolver.resolve(&resolved.profile);
let api_key = match credential {
crate::foundation::credentials::Credential::Keychain(s)
| crate::foundation::credentials::Credential::Environment(s) => s,
crate::foundation::credentials::Credential::Unavailable(e) => {
return Err(crate::foundation::FoundationError::KeychainUnavailable(
e.to_string(),
));
}
};
let provider_name = resolved.profile.provider.as_str();
let provider: Arc<dyn oxicode_ai::Provider> = Arc::from(
oxicode_ai::register_builtins::create_builtin_provider_with_options(
provider_name,
Some(&api_key),
None,
)
.ok_or_else(|| {
crate::foundation::FoundationError::IncompatibleHost(provider_name.to_string())
})?,
);
tracing::info!(
provider = provider_name,
model = %resolved.profile.model,
source = ?resolved.source,
"Foundation profile resolved with Keychain credential"
);
Ok(provider)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paths_are_consistent() {
let p = OxicodePaths::from_home("/tmp/oxicode-test");
assert!(p.auth.starts_with("/tmp/oxicode-test"));
assert!(p.config.starts_with("/tmp/oxicode-test"));
assert!(p.sessions.starts_with("/tmp/oxicode-test"));
assert!(p.skills.starts_with("/tmp/oxicode-test"));
}
#[tokio::test]
async fn build_oxicode_succeeds() {
let tmp = tempfile::TempDir::new().unwrap();
let paths = OxicodePaths::from_home(tmp.path());
let oxicode = build_oxicode(&paths, None, None).await.unwrap();
let _ = oxicode.ports().state;
}
}