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