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