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