Skip to main content

oxios_kernel/kernel_handle/
mod.rs

1//! Kernel facade — domain Facades composing the System Call API.
2
3pub mod a2a_api;
4pub mod agent_api;
5pub mod calendar_api;
6pub mod email_api;
7pub mod engine_api;
8pub mod exec_api;
9pub mod extension_api;
10pub mod infra_api;
11pub mod knowledge_lens;
12pub mod marketplace_api;
13pub mod mcp_api;
14pub mod memory_api;
15pub mod mount_api;
16pub mod persona_api;
17pub mod project_api;
18pub mod security_api;
19pub mod state_api;
20
21pub use a2a_api::A2aApi;
22pub use agent_api::AgentApi;
23pub use calendar_api::CalendarApi;
24pub use email_api::EmailApi;
25pub use engine_api::{
26    EngineApi, EngineConfigResponse, FallbackEvent, InputModality, ModelInfo, ProviderCategory,
27    ProviderInfo, RoutingConfigSnapshot, RoutingStats, RoutingStatsSnapshot, RoutingUpdate,
28    ValidateKeyResult,
29};
30pub use exec_api::ExecApi;
31pub use exec_api::SharedExecConfig;
32pub use extension_api::ExtensionApi;
33pub use infra_api::InfraApi;
34pub use knowledge_lens::{
35    CopilotResponse, KnowledgeContext, KnowledgeLens, KnowledgeNote, MemoryNote,
36};
37pub use marketplace_api::MarketplaceApi;
38pub use mcp_api::McpApi;
39pub use memory_api::MemoryApi;
40pub use mount_api::{MountApi, MountInfo};
41pub use persona_api::PersonaApi;
42pub use project_api::{ProjectApi, ProjectInfo};
43pub use security_api::SecurityApi;
44pub use state_api::StateApi;
45
46use crate::git_layer::CommitInfo;
47use crate::readiness::ReadinessGate;
48use serde::Serialize;
49use std::sync::Arc;
50
51/// Oxios kernel System Call API — composed of domain Facades.
52///
53/// Each Facade groups related system calls:
54/// - [`StateApi`]     — data persistence, sessions
55/// - [`AgentApi`]     — agent lifecycle, budgets, memory
56/// - [`SecurityApi`]  — auth, audit trail, RBAC, approvals
57/// - [`PersonaApi`]   — multi-persona management
58/// - [`ExtensionApi`] — programs, skills, host tools
59/// - [`McpApi`]       — MCP server bridge
60/// - [`MountApi`]      — Mount (path alias) management (RFC-025)
61/// - [`ProjectApi`]    — Project management, memory linking
62/// - [`ExecApi`]      — execution config, access management
63/// - [`A2aApi`]       — agent-to-agent communication
64/// - [`EngineApi`]    — LLM engine providers, models, config
65/// - [`KnowledgeBase`] — markdown note management (kernel-free, via oxios-markdown)
66pub struct KernelHandle {
67    /// State management: save/load/sessions.
68    pub state: StateApi,
69    /// Agent management: lifecycle/budgets/memory.
70    pub agents: AgentApi,
71    /// Security: auth/audit/RBAC/approvals.
72    pub security: SecurityApi,
73    /// Persona management.
74    pub persona: PersonaApi,
75    /// Extensions: programs/skills/host tools.
76    pub extensions: ExtensionApi,
77    /// MCP server bridge.
78    pub mcp: McpApi,
79    /// Infrastructure: Git/scheduler/cron/resources/events/system.
80    pub infra: InfraApi,
81    /// Project management: work context (RFC-011).
82    pub projects: Option<ProjectApi>,
83    /// Mount management: path aliases (RFC-025).
84    pub mounts: Option<MountApi>,
85    /// Execution: config + access management.
86    pub exec: ExecApi,
87    /// Agent-to-agent communication.
88    pub a2a: A2aApi,
89    /// Engine: LLM providers, models, config.
90    pub engine: EngineApi,
91    /// Knowledge base: markdown notes (direct access, no kernel dependency).
92    pub knowledge: Arc<oxios_markdown::KnowledgeBase>,
93    /// Semantic knowledge overlay (HNSW index + agent recall).
94    pub knowledge_lens: Arc<KnowledgeLens>,
95    /// Marketplace API — ClawHub search, install, update.
96    pub marketplace_api: MarketplaceApi,
97    /// Calendar events — create, update, delete, list, search, freebusy.
98    pub calendar: Option<CalendarApi>,
99    /// Email — send HTML emails via SMTP, template management.
100    pub email: Option<EmailApi>,
101    /// RFC-024 SP4: subsystem readiness gate.
102    pub readiness: Arc<ReadinessGate>,
103}
104
105impl KernelHandle {
106    /// Create a new KernelHandle from 13 domain Facades.
107    ///
108    /// Each Facade is assembled independently in `kernel.rs` and passed here.
109    /// This enables testing individual Facades without the full kernel.
110    #[allow(clippy::too_many_arguments)]
111    pub fn new(
112        state: StateApi,
113        agents: AgentApi,
114        security: SecurityApi,
115        persona: PersonaApi,
116        extensions: ExtensionApi,
117        mcp: McpApi,
118        infra: InfraApi,
119        projects: Option<ProjectApi>,
120        exec: ExecApi,
121        a2a: A2aApi,
122        engine: EngineApi,
123        knowledge: Arc<oxios_markdown::KnowledgeBase>,
124        knowledge_lens: Arc<KnowledgeLens>,
125        marketplace_api: MarketplaceApi,
126        calendar: Option<CalendarApi>,
127        email: Option<EmailApi>,
128    ) -> Self {
129        Self {
130            state,
131            agents,
132            security,
133            persona,
134            extensions,
135            mcp,
136            infra,
137            projects,
138            mounts: None,
139            exec,
140            a2a,
141            engine,
142            knowledge,
143            knowledge_lens,
144            marketplace_api,
145            calendar,
146            email,
147            // RFC-024 SP4: default Warming/no-deadline. The Kernel
148            // (src/kernel.rs) sets the actual state and deadline during
149            // startup via `readiness.set_*` / a background task.
150            readiness: Arc::new(ReadinessGate::new(0)),
151        }
152    }
153
154    /// Attach a MountManager-backed API (RFC-025).
155    ///
156    /// Called by the kernel assembler after SQLite initializes the
157    /// `MountManager`. Leaves the [`Self::projects`] facade untouched so
158    /// RFC-011 Projects continue to work during the migration.
159    pub fn with_mounts(mut self, mounts: MountApi) -> Self {
160        self.mounts = Some(mounts);
161        self
162    }
163
164    /// Set the Mounts facade in place (post-construction wiring).
165    pub fn set_mounts(&mut self, mounts: MountApi) {
166        self.mounts = Some(mounts);
167    }
168
169    // ═══════════════════════════════════════════════════════════════════════
170    // Convenience methods (cross-Facades orchestration)
171    // ═══════════════════════════════════════════════════════════════════════
172
173    /// Save data and commit to git (State + Infra).
174    ///
175    /// The state save is the source of truth and is fully propagated. The git
176    /// commit is best-effort observability: if it fails (full disk, lock
177    /// contention, missing committer identity) we log a warning rather than
178    /// failing the save — the data is already persisted on disk and failing
179    /// here would mislead callers into thinking the save itself failed.
180    pub async fn save_and_commit<T: Serialize>(
181        &self,
182        category: &str,
183        name: &str,
184        data: &T,
185    ) -> anyhow::Result<()> {
186        self.state.save(category, name, data).await?;
187        let git = self.infra.git();
188        if git.is_enabled() {
189            let rel_path = format!("{category}/{name}.json");
190            if let Err(e) = git.commit_file(&rel_path, &format!("save {category}/{name}")) {
191                tracing::warn!(
192                    error = %e, rel_path = %rel_path,
193                    "save_and_commit: git commit failed (data was still saved)"
194                );
195            }
196        }
197        Ok(())
198    }
199
200    /// Save markdown and commit to git (State + Infra).
201    ///
202    /// See [`Self::save_and_commit`] for the git-failure policy.
203    pub async fn save_markdown_and_commit(
204        &self,
205        category: &str,
206        name: &str,
207        content: &str,
208    ) -> anyhow::Result<()> {
209        self.state.save_markdown(category, name, content).await?;
210        let git = self.infra.git();
211        if git.is_enabled() {
212            let rel_path = format!("{category}/{name}.md");
213            if let Err(e) = git.commit_file(&rel_path, &format!("save {category}/{name}")) {
214                tracing::warn!(
215                    error = %e, rel_path = %rel_path,
216                    "save_markdown_and_commit: git commit failed (data was still saved)"
217                );
218            }
219        }
220        Ok(())
221    }
222
223    /// Delete a file and commit the removal to git (State + Infra).
224    ///
225    /// See [`Self::save_and_commit`] for the git-failure policy.
226    pub async fn delete_and_commit(&self, category: &str, name: &str) -> anyhow::Result<bool> {
227        let deleted = self.state.delete(category, name).await?;
228        if deleted {
229            let git = self.infra.git();
230            if git.is_enabled() {
231                let rel_path = format!("{category}/{name}.json");
232                if let Err(e) = git.remove_file(&rel_path, &format!("delete {category}/{name}")) {
233                    tracing::warn!(
234                        error = %e, rel_path = %rel_path,
235                        "delete_and_commit: git remove failed (file was still deleted)"
236                    );
237                }
238            }
239        }
240        Ok(deleted)
241    }
242
243    /// Commit all current changes to git.
244    pub fn commit_all(&self, message: &str) -> anyhow::Result<Option<CommitInfo>> {
245        self.state.commit_all(self.infra.git(), message)
246    }
247
248    /// Flush audit trail and commit to git (Security + Infra).
249    pub fn flush_audit(&self) -> anyhow::Result<()> {
250        self.security.flush(self.infra.git())
251    }
252
253    /// Schedule a cron job by expression (convenience wrapper).
254    ///
255    /// **Note:** the `persona` argument is currently NOT wired into the cron
256    /// executor — `CronJob` has no persona field yet. Passing a non-default
257    /// value logs a warning so callers are not silently surprised. The
258    /// parameter is retained for forward compatibility with multi-persona
259    /// scheduling (RFC tracking).
260    pub async fn schedule(
261        &self,
262        cron_expr: &str,
263        task: &str,
264        persona: Option<&str>,
265    ) -> anyhow::Result<String> {
266        if let Some(p) = persona
267            && !p.is_empty()
268            && p != "default"
269        {
270            tracing::warn!(
271                persona = p,
272                "schedule: persona argument is not yet honored by the cron executor; job will run with the default persona"
273            );
274        }
275        let job = crate::cron::CronJob::new(
276            format!("job_{}", uuid::Uuid::new_v4()),
277            cron_expr.to_string(),
278            task.to_string(),
279        );
280        let job_id = self.infra.add_cron(job).await?;
281        Ok(job_id.to_string())
282    }
283
284    /// Unschedule a cron job by string ID (convenience wrapper).
285    ///
286    /// Returns `Ok(true)` when the job existed and was removed, `Ok(false)`
287    /// when no job with that ID was registered, and `Err(...)` when the
288    /// scheduler itself fails (DB corruption, lock poisoning). The previous
289    /// implementation collapsed scheduler errors into `Ok(false)`, hiding
290    /// real failures from callers.
291    pub async fn unschedule(&self, job_id: &str) -> anyhow::Result<bool> {
292        let uuid =
293            uuid::Uuid::parse_str(job_id).map_err(|e| anyhow::anyhow!("invalid job id: {e}"))?;
294        match self.infra.remove_cron(uuid).await {
295            Ok(()) => Ok(true),
296            Err(e) => {
297                let msg = format!("{e}");
298                if msg.to_lowercase().contains("not found") {
299                    // Legitimate "already removed" case — not an error.
300                    Ok(false)
301                } else {
302                    Err(anyhow::anyhow!("failed to remove cron job {job_id}: {e}"))
303                }
304            }
305        }
306    }
307    pub fn list_schedules(&self) -> Vec<crate::cron::CronJob> {
308        self.infra.list_crons()
309    }
310
311    /// Load JSON from state store.
312    pub async fn load_json<T: serde::de::DeserializeOwned>(
313        &self,
314        category: &str,
315        name: &str,
316    ) -> anyhow::Result<Option<T>> {
317        self.state.load(category, name).await
318    }
319
320    /// Get kernel start time.
321    pub fn start_time(&self) -> std::time::Instant {
322        self.infra.start_time
323    }
324
325    /// Marketplace API — ClawHub search, install, update.
326    pub fn marketplace_api(&self) -> &MarketplaceApi {
327        &self.marketplace_api
328    }
329
330    /// Get a [`MemoryApi`] facade for memory operations.
331    ///
332    /// Returns a fresh `MemoryApi` each call. It shares the same underlying
333    /// `Arc<MemoryManager>` and `Arc<HnswMemoryIndex>` (when attached) as
334    /// `AgentApi`, so semantic search and index rebuilds route through the
335    /// real index rather than the keyword-only fallback.
336    pub fn memory(&self) -> MemoryApi {
337        let mm = self.agents.memory_manager().clone();
338        let hnsw = self.agents.hnsw_index.clone();
339        MemoryApi::new(mm, hnsw)
340    }
341}