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