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