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 crate::host_tools::HostToolsApi;
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};
45
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 parking_lot::RwLock;
53use serde::Serialize;
54use std::sync::Arc;
55
56/// Oxios kernel System Call API — composed of domain Facades.
57///
58/// Each Facade groups related system calls:
59/// - [`StateApi`] — data persistence, sessions
60/// - [`AgentApi`] — agent lifecycle, budgets, memory
61/// - [`SecurityApi`] — auth, audit trail, RBAC, approvals
62/// - [`PersonaApi`] — multi-persona management
63/// - [`ExtensionApi`] — programs, skills, host tools
64/// - [`McpApi`] — MCP server bridge
65/// - [`MountApi`] — Mount (path alias) management (RFC-025)
66/// - [`ProjectApi`] — Project management, memory linking
67/// - [`ExecApi`] — execution config, access management
68/// - [`A2aApi`] — agent-to-agent communication
69/// - [`EngineApi`] — LLM engine providers, models, config
70/// - [`KnowledgeBase`] — markdown note management (kernel-free, via oxios-markdown)
71pub struct KernelHandle {
72 /// State management: save/load/sessions.
73 pub state: StateApi,
74 /// Agent management: lifecycle/budgets/memory.
75 pub agents: AgentApi,
76 /// Security: auth/audit/RBAC/approvals.
77 pub security: SecurityApi,
78 /// Persona management.
79 pub persona: PersonaApi,
80 /// Extensions: programs/skills/host tools.
81 pub extensions: ExtensionApi,
82 /// MCP server bridge.
83 pub mcp: McpApi,
84 /// Infrastructure: Git/scheduler/cron/resources/events/system.
85 pub infra: InfraApi,
86 /// Project management: work context (RFC-011).
87 pub projects: Option<ProjectApi>,
88 /// Mount management: path aliases (RFC-025).
89 pub mounts: Option<MountApi>,
90 /// Execution: config + access management.
91 pub exec: ExecApi,
92 /// Agent-to-agent communication.
93 pub a2a: A2aApi,
94 /// Engine: LLM providers, models, config.
95 pub engine: EngineApi,
96 /// Knowledge base: markdown notes (direct access, no kernel dependency).
97 pub knowledge: Arc<oxios_markdown::KnowledgeBase>,
98 /// Semantic knowledge overlay (HNSW index + agent recall).
99 pub knowledge_lens: Arc<KnowledgeLens>,
100 /// Marketplace API — ClawHub search, install, update.
101 pub marketplace_api: MarketplaceApi,
102 /// Calendar events — create, update, delete, list, search, freebusy.
103 pub calendar: Option<CalendarApi>,
104 /// Email — send HTML emails via SMTP, template management.
105 pub email: Arc<RwLock<Option<EmailApi>>>,
106 /// Token-maxing (RFC-031): the shared QuotaTracker facade. `None` only on
107 /// the incomplete preliminary handle; the cached handle attaches it.
108 pub token_maxing: Option<TokenMaxingApi>,
109 /// Host Integrations (RFC-041): host-CLI discovery, OAuth, provisioning.
110 pub host_tools: HostToolsApi,
111 /// RFC-024 SP4: subsystem readiness gate.
112 pub readiness: Arc<ReadinessGate>,
113 /// Per-session streaming sink registry (P1 chat transparency).
114 ///
115 /// The agent runtime callback looks up the sink by `session_id` (which
116 /// it already has via `transparency_session`) and pushes live text
117 /// deltas. The gateway registers a strong sender before invoking
118 /// the orchestrator and drops it after the collector completes; the
119 /// `Weak` entries auto-clean.
120 pub streaming_sinks: Arc<crate::streaming_sink::StreamingSinkRegistry>,
121}
122
123impl KernelHandle {
124 /// Create a new KernelHandle from 14 domain Facades.
125 ///
126 /// Each Facade is assembled independently in `kernel.rs` and passed here.
127 /// This enables testing individual Facades without the full kernel.
128 #[allow(clippy::too_many_arguments)]
129 pub fn new(
130 state: StateApi,
131 agents: AgentApi,
132 security: SecurityApi,
133 persona: PersonaApi,
134 extensions: ExtensionApi,
135 mcp: McpApi,
136 infra: InfraApi,
137 projects: Option<ProjectApi>,
138 exec: ExecApi,
139 a2a: A2aApi,
140 engine: EngineApi,
141 knowledge: Arc<oxios_markdown::KnowledgeBase>,
142 knowledge_lens: Arc<KnowledgeLens>,
143 marketplace_api: MarketplaceApi,
144 calendar: Option<CalendarApi>,
145 email: Arc<RwLock<Option<EmailApi>>>,
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 a2a,
159 engine,
160 knowledge,
161 knowledge_lens,
162 marketplace_api,
163 calendar,
164 email,
165 token_maxing: None,
166 host_tools: HostToolsApi::new(),
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}