Skip to main content

oxicode_sdk/
builder.rs

1//! OxicodeBuilder and Oxicode — SDK entry point
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use oxicode_agent::{ProviderResolver, ToolRegistry};
7use oxicode_ai::{Model, ModelRegistry, Provider, ProviderRegistry};
8
9use crate::agent_builder::AgentBuilder;
10use crate::error::{SdkError, SdkResult};
11use crate::lifecycle::{AgentSupervisor, FileSnapshotStore, SupervisorPolicy};
12use crate::ports::PortRegistry;
13
14/// Oxicode AI engine instance — holds isolated provider and model registries.
15///
16/// Created via [`OxicodeBuilder`]. Provides access to providers, models,
17/// provider creation, and agent building.
18///
19/// Implements [`ProviderResolver`] so it can be passed directly to
20/// [`oxicode_agent::Agent::new_with_resolver`] for fully isolated operation.
21#[derive(Clone)]
22pub struct Oxicode {
23    providers: Arc<ProviderRegistry>,
24    models: Arc<ModelRegistry>,
25    tools: Arc<ToolRegistry>,
26    /// Whether built-in providers are enabled (`OxicodeBuilder::with_builtins`).
27    include_builtins: bool,
28    /// Per-provider API key overrides (`OxicodeBuilder::api_key`).
29    api_keys: Arc<HashMap<String, String>>,
30    /// Per-provider base URL overrides (`OxicodeBuilder::base_url`).
31    base_urls: Arc<HashMap<String, String>>,
32    /// Port registry (None = use noop default).
33    ports: PortRegistry,
34    /// MCP manager (Phase 1+). `None` if MCP is disabled or has not been
35    /// spawned yet.
36    mcp_manager: Option<Arc<oxicode_agent::mcp::McpManager>>,
37    /// Live routing state. `Arc` so external holders (the supervisor,
38    /// agent builders, host apps) share the same instance and see
39    /// each other's mutations. Resolution-time exclusion of models
40    /// declared in `excluded_models` consults this field.
41    routing: Arc<crate::routing::RoutingControl>,
42}
43
44impl Oxicode {
45    /// Create an agent builder with the given config.
46    pub fn agent(&self, config: oxicode_agent::AgentConfig) -> AgentBuilder<'_> {
47        AgentBuilder::new(self, config)
48    }
49
50    /// Get the provider registry.
51    pub fn providers(&self) -> &ProviderRegistry {
52        &self.providers
53    }
54
55    /// Get the model registry.
56    pub fn models(&self) -> &ModelRegistry {
57        &self.models
58    }
59
60    /// Get the shared tool registry.
61    pub fn tools(&self) -> Arc<ToolRegistry> {
62        Arc::clone(&self.tools)
63    }
64
65    /// Get the port registry (state, config, auth, event bus, ...).
66    pub fn ports(&self) -> &PortRegistry {
67        &self.ports
68    }
69
70    /// Catalog port accessor. Use this for all catalog queries.
71    ///
72    /// Returns a reference to the `Arc<dyn ModelCatalog>`. The default
73    /// (when `OxicodeBuilder::with_catalog()` is not called) is a
74    /// [`NoopModelCatalog`](crate::ports::catalog::NoopModelCatalog) —
75    /// all lookups return empty/None.
76    ///
77    /// # Example
78    ///
79    /// ```no_run
80    /// # async fn doc(oxicode: oxicode_sdk::Oxicode) -> Result<(), oxicode_sdk::SdkError> {
81    /// let providers = oxicode.catalog().list_providers().await?;
82    /// let model = oxicode.catalog().get_model("anthropic", "claude-sonnet-4-20250514").await?;
83    /// # Ok(()) }
84    /// ```
85    pub fn catalog(&self) -> &Arc<dyn crate::ports::catalog::ModelCatalog> {
86        &self.ports.catalog
87    }
88
89    /// Get the MCP manager, if MCP is enabled.
90    ///
91    /// This is the entry point for SDK consumers who want to use MCP from
92    /// outside the agent loop — e.g. the TUI dashboard, RPC handlers, or
93    /// custom agent integrations.
94    ///
95    /// Returns `None` if MCP was disabled via [`OxicodeBuilder::with_mcp`] with
96    /// `false`.
97    pub fn mcp(&self) -> Option<Arc<oxicode_agent::mcp::McpManager>> {
98        self.mcp_manager.clone()
99    }
100
101    /// Resolve a model ID to a Model.
102    ///
103    /// Accepts `"provider/model"` or bare `"model"` (defaults to "anthropic").
104    ///
105    /// Resolution order:
106    /// 1. The catalog port (if wired) — reads the in-memory snapshot.
107    /// 2. The static model registry (`with_builtins`).
108    ///
109    /// The `routing.excluded_models` list is consulted **before** the
110    /// catalog/static lookups — `set_enabled(false)` / `exclude_model`
111    /// / `unexclude_model` on the shared `RoutingControl` instance
112    /// take effect on the next resolution.
113    pub fn resolve_model(&self, model_id: &str) -> SdkResult<Model> {
114        // Live routing exclusion: ONLY active when is_enabled().
115        // `set_enabled(false)` is an explicit opt-out — it means
116        // "skip routing rules, resolve normally," NOT "refuse to
117        // resolve." The default Oxicode (RoutingControl::default) has
118        // auto_routing=true, so this gate is a no-op unless the
119        // host explicitly disabled routing.
120        if self.routing.is_enabled() && self.routing.excluded_models().iter().any(|m| m == model_id)
121        {
122            return Err(SdkError::ModelExcluded {
123                model_id: model_id.to_string(),
124            });
125        }
126
127        let parts: Vec<&str> = model_id.splitn(2, '/').collect();
128        let (provider, model) = if parts.len() == 2 {
129            (parts[0], parts[1])
130        } else {
131            ("anthropic", parts[0])
132        };
133
134        // 1. Catalog port (sync read of the snapshot).
135        if let Some(ref entry) = self.ports.catalog.get_model_sync(provider, model) {
136            return Ok(crate::bridge::catalog_entry_to_model(provider, entry));
137        }
138
139        // 2. Static model registry fallback.
140        self.models
141            .lookup(provider, model)
142            .ok_or_else(|| SdkError::ModelNotFound {
143                model_id: model_id.to_string(),
144            })
145    }
146
147    /// 1. Custom providers registered via `OxicodeBuilder::provider()`
148    /// 2. Provider factories registered via `OxicodeBuilder::provider_factory()`
149    /// 3. Built-in providers with credential injection (if `with_builtins()` was called):
150    ///    a. Explicit per-provider key from `OxicodeBuilder::api_key(name, key)`
151    ///    b. The wired `AuthProvider` port (sync fast-path). This is the
152    ///    primary credential source for products like the CLI, which
153    ///    never call `OxicodeBuilder::api_key()` and instead register
154    ///    `FileAuthProvider` via `.with_auth(...)`. Consulted on every
155    ///    `create_provider` call, so auth-store updates (e.g. a key entered
156    ///    via the TUI overlay) are picked up without rebuilding the engine.
157    ///    c. Provider env var (the `create_builtin_provider_with_options`
158    ///    fallback inside `oxicode-ai`).
159    ///
160    /// This is the **single credential authority** for the agent loop: the
161    /// `AgentConfig.api_key` field and the `api_key` params on
162    /// `Agent::switch_model` / `Agent::refresh_api_key` are vestigial after
163    /// this wiring and are removed in a follow-up. See issues #39 and #40.
164    pub fn create_provider(&self, name: &str) -> SdkResult<Arc<dyn Provider>> {
165        // 1. Check custom providers registered via OxicodeBuilder::provider()
166        if let Some(p) = self.providers.get_custom(name) {
167            return Ok(p);
168        }
169        // 2. Built-in providers with credential injection.
170        if self.include_builtins {
171            let base_url = self.base_urls.get(name).map(|s| s.as_str());
172            // Credential resolution: explicit OxicodeBuilder::api_key() override first,
173            // then the AuthProvider port's sync fast-path, then env-var fallback
174            // (handled inside create_builtin_provider_with_options).
175            let explicit_key = self.api_keys.get(name).map(|s| s.as_str());
176            let auth_port_key = self
177                .ports
178                .auth
179                .get_api_key_sync(name)
180                .ok()
181                .flatten()
182                .filter(|s| !s.is_empty());
183            let api_key = explicit_key.or(auth_port_key.as_deref());
184            if let Some(p) =
185                oxicode_ai::create_builtin_provider_with_options(name, api_key, base_url)
186            {
187                return Ok(Arc::from(p));
188            }
189            // Fallback to default built-in creation (no credential override)
190            if let Some(p) = oxicode_ai::create_builtin_provider(name) {
191                return Ok(Arc::from(p));
192            }
193        }
194        Err(SdkError::ProviderNotFound {
195            provider: name.to_string(),
196        })
197    }
198
199    /// Get the provider registry (Arc clone).
200    pub fn providers_arc(&self) -> Arc<ProviderRegistry> {
201        Arc::clone(&self.providers)
202    }
203
204    /// Get the model registry (Arc clone).
205    pub fn models_arc(&self) -> Arc<ModelRegistry> {
206        Arc::clone(&self.models)
207    }
208
209    /// Check whether built-in providers are enabled.
210    pub fn has_builtins(&self) -> bool {
211        self.include_builtins
212    }
213
214    /// Borrow the shared [`crate::routing::RoutingControl`] instance. Use this to
215    /// call `set_enabled`, `exclude_model`, `set_fallback_models`, etc.
216    /// Mutations are observed by the next model/provider resolution.
217    pub fn routing(&self) -> &Arc<crate::routing::RoutingControl> {
218        &self.routing
219    }
220}
221
222/// Implement ProviderResolver so Oxicode can be used as Agent's resolver.
223impl ProviderResolver for Oxicode {
224    fn resolve_provider(&self, name: &str) -> Option<Arc<dyn Provider>> {
225        self.create_provider(name).ok()
226    }
227
228    fn resolve_model(&self, model_id: &str) -> Option<Model> {
229        self.resolve_model(model_id).ok()
230    }
231}
232
233/// Builder for creating an Oxicode instance.
234pub struct OxicodeBuilder {
235    providers: ProviderRegistry,
236    models: ModelRegistry,
237    tools: ToolRegistry,
238    include_builtins: bool,
239    api_keys: HashMap<String, String>,
240    base_urls: HashMap<String, String>,
241    /// Port registry (None = use noop default).
242    ports: Option<PortRegistry>,
243    /// Programmatic MCP config (overrides the on-disk config if set).
244    mcp_config: Option<oxicode_agent::mcp::McpConfig>,
245    /// Whether MCP is enabled. Defaults to true (when `with_builtins()` is
246    /// also called) or as set by `with_mcp(false)`.
247    mcp_enabled: bool,
248    /// Custom disk path for the MCP metadata cache. When unset, oxicode uses
249    /// its default (`~/.config/oxicode/mcp-cache.json`).
250    mcp_cache_path: Option<std::path::PathBuf>,
251    /// Custom disk path for the MCP consent store. When unset, oxicode uses
252    /// its default (`~/.config/oxicode/mcp-consent.json`).
253    mcp_consent_path: Option<std::path::PathBuf>,
254}
255
256impl OxicodeBuilder {
257    /// Create a new empty builder (no builtins, no providers, no models).
258    pub fn new() -> Self {
259        Self {
260            providers: ProviderRegistry::new(),
261            models: ModelRegistry::new(),
262            tools: ToolRegistry::new(),
263            include_builtins: false,
264            api_keys: HashMap::new(),
265            base_urls: HashMap::new(),
266            ports: None,
267            mcp_config: None,
268            mcp_enabled: true,
269            mcp_cache_path: None,
270            mcp_consent_path: None,
271        }
272    }
273
274    /// Register all built-in models and enable built-in provider creation.
275    ///
276    /// This loads 50+ model definitions from the oxicode-ai static database
277    /// and enables `create_builtin_provider()` fallback in [`Oxicode::create_provider`].
278    pub fn with_builtins(mut self) -> Self {
279        self.models = ModelRegistry::from_static();
280        self.include_builtins = true;
281        self
282    }
283
284    /// Register a custom provider.
285    pub fn provider(self, name: &str, p: impl Provider + 'static) -> Self {
286        self.providers.register(name, p);
287        self
288    }
289
290    /// Register a custom tool in the shared tool registry.
291    pub fn tool(self, tool: impl oxicode_agent::AgentTool + 'static) -> Self {
292        self.tools.register(tool);
293        self
294    }
295
296    /// Register a provider factory — a closure that lazily creates a provider.
297    ///
298    /// Unlike [`Self::provider()`], which takes an already-constructed instance,
299    /// this stores a factory closure. The factory is invoked the **first time**
300    /// `Oxicode::create_provider(name)` is called, and the resulting provider is
301    /// cached for subsequent calls.
302    ///
303    /// This is useful when provider construction requires credential resolution
304    /// or network configuration that should happen at first use, not at build time.
305    ///
306    /// # Example
307    ///
308    /// ```no_run
309    /// use std::sync::Arc;
310    /// use oxicode_sdk::{OxicodeBuilder, OpenAiProvider};
311    ///
312    /// let oxicode = OxicodeBuilder::new()
313    ///     .with_builtins()
314    ///     .provider_factory("custom", || {
315    ///         Ok(Arc::new(OpenAiProvider::with_base_url_and_key(
316    ///             "https://api.example.com",
317    ///             Some("key".into()),
318    ///         )))
319    ///     })
320    ///     .build();
321    /// ```
322    pub fn provider_factory(
323        self,
324        name: &str,
325        factory: impl Fn() -> anyhow::Result<Arc<dyn Provider>> + Send + Sync + 'static,
326    ) -> Self {
327        self.providers.register_factory(name, factory);
328        self
329    }
330
331    /// Register an API key for a specific provider.
332    ///
333    /// When `create_provider(name)` is called, the key is injected into
334    /// the provider's constructor automatically. Keys registered here
335    /// take precedence over environment variables.
336    ///
337    /// # Example
338    ///
339    /// ```rust
340    /// use oxicode_sdk::OxicodeBuilder;
341    ///
342    /// let oxicode = OxicodeBuilder::new()
343    ///     .with_builtins()
344    ///     .api_key("anthropic", "sk-ant-test-key")
345    ///     .api_key("openai", "sk-test-key")
346    ///     .build();
347    /// ```
348    pub fn api_key(mut self, provider_name: &str, key: impl Into<String>) -> Self {
349        self.api_keys.insert(provider_name.to_string(), key.into());
350        self
351    }
352
353    /// Register a base URL override for a specific provider.
354    ///
355    /// Useful for OpenAI-compatible providers (ZAI, Groq, etc.)
356    /// that use a different endpoint.
357    ///
358    /// # Example
359    ///
360    /// ```rust
361    /// use oxicode_sdk::OxicodeBuilder;
362    ///
363    /// let oxicode = OxicodeBuilder::new()
364    ///     .with_builtins()
365    ///     .base_url("openai", "https://my-proxy.example.com/v1")
366    ///     .build();
367    /// ```
368    pub fn base_url(mut self, provider_name: &str, url: impl Into<String>) -> Self {
369        self.base_urls.insert(provider_name.to_string(), url.into());
370        self
371    }
372
373    /// Register a full credential set for a provider.
374    ///
375    /// Convenience method combining [`api_key()`](Self::api_key) and
376    /// [`base_url()`](Self::base_url).
377    ///
378    /// # Example
379    ///
380    /// ```rust
381    /// use oxicode_sdk::OxicodeBuilder;
382    ///
383    /// let oxicode = OxicodeBuilder::new()
384    ///     .with_builtins()
385    ///     .credential("openai", "sk-test", Some("https://proxy.example.com/v1"))
386    ///     .build();
387    /// ```
388    pub fn credential(
389        self,
390        provider_name: &str,
391        api_key: impl Into<String>,
392        base_url: Option<&str>,
393    ) -> Self {
394        let mut builder = self.api_key(provider_name, api_key);
395        if let Some(url) = base_url {
396            builder = builder.base_url(provider_name, url);
397        }
398        builder
399    }
400
401    /// Register a custom model.
402    pub fn model(self, model: Model) -> Self {
403        self.models.register(model);
404        self
405    }
406
407    // ─── Port registration ────────────────────────────────────────────────
408    //
409    // Products (oxicode-cli, oxios-kernel, custom apps) register concrete
410    // implementations of the port traits defined in `crate::ports`.
411    // All ports are optional: unset ports use a noop default.
412
413    /// Register a complete [`PortRegistry`] at once.
414    ///
415    /// Use this when you have a fully-built registry (e.g. loaded from a
416    /// directory of file-based adapters). For piecemeal registration, use
417    /// the `with_port_*` methods below.
418    pub fn with_ports(mut self, ports: PortRegistry) -> Self {
419        self.ports = Some(ports);
420        self
421    }
422
423    /// Register the model catalog port.
424    ///
425    /// The catalog is the source of truth for provider/model metadata.
426    /// If not called, the SDK uses [`NoopModelCatalog`](crate::ports::catalog::NoopModelCatalog)
427    /// (empty results — all lookups return `None`/`vec![]`).
428    ///
429    /// # Example
430    ///
431    /// ```no_run
432    /// use oxicode_sdk::{OxicodeBuilder, NoopModelCatalog};
433    ///
434    /// // `NoopModelCatalog` is the empty default used when no catalog is
435    /// // registered — pass any `Arc<dyn ModelCatalog>` here instead.
436    /// let catalog = NoopModelCatalog::new();
437    /// let oxicode = OxicodeBuilder::new()
438    ///     .with_catalog(catalog)
439    ///     .build();
440    /// ```
441    pub fn with_catalog(mut self, catalog: Arc<dyn crate::ports::catalog::ModelCatalog>) -> Self {
442        let mut ports = self.ports.unwrap_or_default();
443        ports.catalog = catalog;
444        self.ports = Some(ports);
445        self
446    }
447
448    /// Register the state store.
449    pub fn with_state(mut self, store: Arc<dyn crate::ports::StateStore>) -> Self {
450        let mut ports = self.ports.unwrap_or_default();
451        ports.state = store;
452        self.ports = Some(ports);
453        self
454    }
455
456    /// Register the config store.
457    pub fn with_config(mut self, store: Arc<dyn crate::ports::ConfigStore>) -> Self {
458        let mut ports = self.ports.unwrap_or_default();
459        ports.config = store;
460        self.ports = Some(ports);
461        self
462    }
463
464    /// Register the auth provider.
465    pub fn with_auth(mut self, auth: Arc<dyn crate::ports::AuthProvider>) -> Self {
466        let mut ports = self.ports.unwrap_or_default();
467        ports.auth = auth;
468        self.ports = Some(ports);
469        self
470    }
471
472    /// Register the event bus.
473    pub fn with_event_bus(mut self, bus: Arc<dyn crate::ports::EventBus>) -> Self {
474        let mut ports = self.ports.unwrap_or_default();
475        ports.event_bus = bus;
476        self.ports = Some(ports);
477        self
478    }
479
480    /// Register the skill loader.
481    pub fn with_skills(mut self, loader: Arc<dyn crate::ports::SkillLoader>) -> Self {
482        let mut ports = self.ports.unwrap_or_default();
483        ports.skills = loader;
484        self.ports = Some(ports);
485        self
486    }
487
488    /// Register the persona provider.
489    pub fn with_personas(mut self, provider: Arc<dyn crate::ports::PersonaProvider>) -> Self {
490        let mut ports = self.ports.unwrap_or_default();
491        ports.personas = provider;
492        self.ports = Some(ports);
493        self
494    }
495
496    /// Register the access gate.
497    pub fn with_access(mut self, gate: Arc<dyn crate::ports::AccessGate>) -> Self {
498        let mut ports = self.ports.unwrap_or_default();
499        ports.access = gate;
500        self.ports = Some(ports);
501        self
502    }
503
504    /// Register the capability resolver.
505    pub fn with_capabilities(
506        mut self,
507        resolver: Arc<dyn crate::ports::CapabilityResolver>,
508    ) -> Self {
509        let mut ports = self.ports.unwrap_or_default();
510        ports.capabilities = resolver;
511        self.ports = Some(ports);
512        self
513    }
514
515    /// Register the memory store.
516    pub fn with_memory(mut self, store: Arc<dyn crate::ports::MemoryStore>) -> Self {
517        let mut ports = self.ports.unwrap_or_default();
518        ports.memory = store;
519        self.ports = Some(ports);
520        self
521    }
522
523    /// Register the cron scheduler.
524    pub fn with_cron(mut self, scheduler: Arc<dyn crate::ports::CronScheduler>) -> Self {
525        let mut ports = self.ports.unwrap_or_default();
526        ports.cron = scheduler;
527        self.ports = Some(ports);
528        self
529    }
530
531    /// Register the resource monitor.
532    pub fn with_resources(mut self, monitor: Arc<dyn crate::ports::ResourceMonitor>) -> Self {
533        let mut ports = self.ports.unwrap_or_default();
534        ports.resources = monitor;
535        self.ports = Some(ports);
536        self
537    }
538
539    /// Register the internal URL router.
540    pub fn with_url_router(mut self, router: Arc<dyn crate::ports::InternalUrlRouter>) -> Self {
541        let mut ports = self.ports.unwrap_or_default();
542        ports.url_router = router;
543        self.ports = Some(ports);
544        self
545    }
546
547    /// Register the rule registry (TTSR).
548    pub fn with_rules(mut self, rules: Arc<dyn crate::ports::RuleRegistry>) -> Self {
549        let mut ports = self.ports.unwrap_or_default();
550        ports.rules = rules;
551        self.ports = Some(ports);
552        self
553    }
554
555    /// Register the embedding provider.
556    pub fn with_embeddings(mut self, embeddings: Arc<dyn crate::ports::EmbeddingProvider>) -> Self {
557        let mut ports = self.ports.unwrap_or_default();
558        ports.embeddings = embeddings;
559        self.ports = Some(ports);
560        self
561    }
562
563    /// Register the hook runner port.
564    ///
565    /// When set, [`crate::AgentBuilder::with_port_hooks`] composes a
566    /// [`HookMiddleware`](crate::middleware::HookMiddleware) backed by
567    /// this runner into the agent's hook pipeline. When unset, the port
568    /// stays at [`NoopHookRunner`](crate::ports::NoopHookRunner) and the
569    /// middleware short-circuits to a no-op.
570    pub fn with_hooks(mut self, runner: Arc<dyn crate::ports::HookRunner>) -> Self {
571        let mut ports = self.ports.unwrap_or_default();
572        ports.hooks = runner;
573        self.ports = Some(ports);
574        self
575    }
576
577    /// Create a supervisor builder for managing agent lifecycles.
578    ///
579    /// # Example
580    ///
581    /// ```ignore
582    /// use oxicode_sdk::OxicodeBuilder;
583    ///
584    /// let (oxicode, supervisor) = OxicodeBuilder::new()
585    ///     .with_builtins()
586    ///     .supervisor()
587    ///     .snapshot_dir("/data/snapshots")
588    ///     .build()?;
589    /// ```
590    pub fn supervisor(self) -> SupervisorBuilder {
591        SupervisorBuilder {
592            oxicode_builder: self,
593            policy: SupervisorPolicy::default(),
594            snapshot_dir: None,
595            agent_decorator: None,
596        }
597    }
598    /// Build the Oxicode engine. This consumes the builder.
599    pub fn build(self) -> Oxicode {
600        // Spawn the MCP manager unless explicitly disabled.
601        let mcp_manager = if self.mcp_enabled {
602            if self.mcp_cache_path.is_some() || self.mcp_consent_path.is_some() {
603                let cfg = match self.mcp_config {
604                    Some(cfg) => cfg,
605                    None => oxicode_agent::mcp::config::load_mcp_config(),
606                };
607                Some(oxicode_agent::mcp::McpManager::spawn_with_paths(
608                    cfg,
609                    self.mcp_cache_path,
610                    self.mcp_consent_path,
611                ))
612            } else {
613                Some(match self.mcp_config {
614                    Some(cfg) => oxicode_agent::mcp::McpManager::spawn_with_config(cfg),
615                    None => oxicode_agent::mcp::McpManager::spawn(),
616                })
617            }
618        } else {
619            None
620        };
621
622        Oxicode {
623            providers: Arc::new(self.providers),
624            models: Arc::new(self.models),
625            tools: Arc::new(self.tools),
626            include_builtins: self.include_builtins,
627            api_keys: Arc::new(self.api_keys),
628            base_urls: Arc::new(self.base_urls),
629            ports: self.ports.unwrap_or_default(),
630            mcp_manager,
631            routing: Arc::new(crate::routing::RoutingControl::new(
632                crate::routing::RoutingConfig::default(),
633            )),
634        }
635    }
636
637    // ── MCP configuration (Phase SDK) ───────────────────────────────
638
639    /// Inject a programmatic MCP configuration. This overrides the
640    /// on-disk `~/.config/oxicode/mcp.json` and `.mcp.json` discovery.
641    ///
642    /// # Example
643    ///
644    /// ```no_run
645    /// use oxicode_sdk::{OxicodeBuilder, McpConfig, ServerEntry, LifecycleMode};
646    ///
647    /// let mut mcp = McpConfig::default();
648    /// mcp.mcp_servers.insert(
649    ///     "my-server".into(),
650    ///     ServerEntry {
651    ///         command: Some("npx".into()),
652    ///         args: Some(vec!["-y".into(), "@my-org/mcp-server".into()]),
653    ///         lifecycle: Some(LifecycleMode::Lazy),
654    ///         ..Default::default()
655    ///     },
656    /// );
657    ///
658    /// let oxicode = OxicodeBuilder::new()
659    ///     .with_builtins()
660    ///     .with_mcp_config(mcp)
661    ///     .build();
662    /// ```
663    pub fn with_mcp_config(mut self, config: oxicode_agent::mcp::McpConfig) -> Self {
664        self.mcp_config = Some(config);
665        self.mcp_enabled = true;
666        self
667    }
668
669    /// Set custom disk paths for the MCP metadata cache and consent store.
670    ///
671    /// Only takes effect when MCP is enabled (see [`with_mcp`](Self::with_mcp)).
672    /// When unset, oxicode uses its default paths (`~/.config/oxicode/`). Intended
673    /// for SDK consumers that self-host MCP state under their own config
674    /// directory (e.g. oxios under `~/.oxios/`).
675    ///
676    /// Combine with [`with_mcp_config`](Self::with_mcp_config) to also inject
677    /// a programmatic config. If only paths are supplied (no config), oxicode
678    /// auto-discovers its config from the standard file locations and writes
679    /// cache/consent to the supplied paths.
680    pub fn with_mcp_paths(
681        mut self,
682        cache_path: std::path::PathBuf,
683        consent_path: std::path::PathBuf,
684    ) -> Self {
685        self.mcp_cache_path = Some(cache_path);
686        self.mcp_consent_path = Some(consent_path);
687        self
688    }
689
690    /// Enable or disable MCP. When disabled, no `McpManager` is spawned
691    /// and the `mcp` proxy tool / direct tools are not registered.
692    ///
693    /// Defaults to `true`.
694    pub fn with_mcp(mut self, enabled: bool) -> Self {
695        self.mcp_enabled = enabled;
696        self
697    }
698}
699
700impl Default for OxicodeBuilder {
701    fn default() -> Self {
702        Self::new()
703    }
704}
705
706// ── SupervisorBuilder ──────────────────────────────────────────────────────
707
708/// Builder for creating an `AgentSupervisor`.
709///
710/// Created via [`OxicodeBuilder::supervisor()`].
711pub struct SupervisorBuilder {
712    oxicode_builder: OxicodeBuilder,
713    policy: SupervisorPolicy,
714    snapshot_dir: Option<std::path::PathBuf>,
715    /// Cross-cutting decorator applied to every supervisor-spawned
716    /// agent. `None` (default) keeps the legacy fast path.
717    agent_decorator: Option<Arc<dyn crate::observability::AgentDecorator>>,
718}
719
720impl SupervisorBuilder {
721    /// Set the restart policy.
722    pub fn policy(mut self, policy: SupervisorPolicy) -> Self {
723        self.policy = policy;
724        self
725    }
726
727    /// Set the directory for persisting snapshots.
728    pub fn snapshot_dir(mut self, dir: impl Into<std::path::PathBuf>) -> Self {
729        self.snapshot_dir = Some(dir.into());
730        self
731    }
732
733    /// Attach an [`crate::observability::AgentDecorator`] that wraps every
734    /// supervisor-spawned agent.
735    ///
736    /// When set, [`SupervisorBuilder::build`] clones the built `Oxicode`
737    /// into the supervisor and configures it to route spawns through
738    /// `Oxicode::agent(config)` + `decorator.decorate(builder)` instead
739    /// of the bare `Agent::new(provider, config, tools)` fast path.
740    /// Use [`crate::observability::ObservabilityDecorator`] to bundle audit / authorizer /
741    /// tracer / cost-tracker — those hooks then actually run on
742    /// every spawned agent (no longer silent no-ops).
743    ///
744    /// Replaces the four deprecated no-op setters `with_audit`,
745    /// `with_authorizer`, `with_tracer`, `with_cost_tracker`, which
746    /// emitted `tracing::warn!` and dropped their arguments.
747    pub fn with_agent_decorator(
748        mut self,
749        decorator: Arc<dyn crate::observability::AgentDecorator>,
750    ) -> Self {
751        self.agent_decorator = Some(decorator);
752        self
753    }
754
755    /// Build the supervisor.
756    ///
757    /// Creates an `Oxicode` instance internally and constructs the
758    /// supervisor with a file-based snapshot store. When
759    /// [`with_agent_decorator`](Self::with_agent_decorator) was
760    /// called, the built `Oxicode` is cloned into the supervisor so
761    /// every spawn routes through the decorator.
762    pub fn build(self) -> anyhow::Result<(Oxicode, AgentSupervisor)> {
763        let oxicode = self.oxicode_builder.build();
764        let resolver: Arc<dyn oxicode_agent::ProviderResolver> = Arc::new(oxicode.clone());
765
766        let snapshot_store: Arc<dyn crate::lifecycle::SnapshotStore> = match &self.snapshot_dir {
767            Some(dir) => Arc::new(FileSnapshotStore::new(dir)?),
768            None => Arc::new(FileSnapshotStore::new(
769                std::env::temp_dir().join("oxicode-snapshots"),
770            )?),
771        };
772
773        let supervisor = AgentSupervisor::with_policy(resolver, snapshot_store, self.policy);
774        let supervisor = if let Some(decorator) = self.agent_decorator {
775            supervisor.with_agent_decorator(Arc::new(oxicode.clone()), decorator)
776        } else {
777            supervisor
778        };
779        Ok((oxicode, supervisor))
780    }
781}