Skip to main content

oxios_kernel/kernel_handle/
infra_api.rs

1//! Infra API — Git, cron, resources, events, system.
2
3use crate::ToolMeta;
4use crate::config::OxiosConfig;
5use crate::cron::{CronJob, CronJobUpdate, CronScheduler};
6use crate::event_bus::{EventBus, KernelEvent};
7use crate::git_layer::{GitLayer, LogEntry};
8use crate::resource_monitor::{ResourceMonitor, ResourceSnapshot};
9use crate::tools::{PendingAskUser, PendingPathAccess, PendingToolApprovals, known_tools};
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12
13/// Infrastructure system calls.
14pub struct InfraApi {
15    pub(crate) git_layer: Arc<GitLayer>,
16    pub(crate) cron_scheduler: Arc<CronScheduler>,
17    pub(crate) resource_monitor: Arc<ResourceMonitor>,
18    pub(crate) event_bus: EventBus,
19    pub(crate) config: OxiosConfig,
20    pub(crate) start_time: Instant,
21    /// Hot-reloadable orchestrator config (evolution iterations, score threshold).
22    pub(crate) orchestrator_config: parking_lot::RwLock<crate::config::OrchestratorConfig>,
23    pub(crate) approval_config: Arc<parking_lot::RwLock<crate::approval::ApprovalConfig>>,
24    /// Pending tool approval requests (HitL escalation).
25    pub(crate) pending_tool_approvals: Arc<PendingToolApprovals>,
26    /// Pending `ask_user` requests (RFC-027 agent-driven clarification).
27    pub(crate) pending_ask_user: Arc<PendingAskUser>,
28    /// Pending path-access requests (interactive Mount / temp-allow / deny cards).
29    pub(crate) pending_path_access: Arc<PendingPathAccess>,
30}
31
32impl InfraApi {
33    /// Create a new InfraApi.
34    // Facade construction gathers the independent infrastructure services.
35    //
36    // `approval_config` is an Arc the caller shares across ALL KernelHandle
37    // instances — the preliminary handle (AgentRuntime's ApprovalGate reads
38    // here) and the cached handle (HTTP PATCH /api/security/approval writes
39    // here). Passing it in (rather than deriving a fresh Arc from `config`
40    // inside `new`) mirrors the `pending_tool_approvals` sharing rule: without
41    // it, a mode toggle writes one instance while the gate reads another, so
42    // AutoRun never takes effect and grants never stick.
43    #[allow(clippy::too_many_arguments)]
44    pub fn new(
45        git_layer: Arc<GitLayer>,
46        cron_scheduler: Arc<CronScheduler>,
47        resource_monitor: Arc<ResourceMonitor>,
48        event_bus: EventBus,
49        config: OxiosConfig,
50        start_time: Instant,
51        pending_tool_approvals: Arc<PendingToolApprovals>,
52        pending_ask_user: Arc<PendingAskUser>,
53        approval_config: Arc<parking_lot::RwLock<crate::approval::ApprovalConfig>>,
54        pending_path_access: Arc<PendingPathAccess>,
55    ) -> Self {
56        Self {
57            git_layer,
58            cron_scheduler,
59            resource_monitor,
60            event_bus,
61            config,
62            start_time,
63            orchestrator_config: parking_lot::RwLock::new(
64                crate::config::OrchestratorConfig::default(),
65            ),
66            approval_config,
67            pending_tool_approvals,
68            pending_ask_user,
69            pending_path_access,
70        }
71    }
72    /// Get a reference to the GitLayer.
73    pub fn git(&self) -> &GitLayer {
74        &self.git_layer
75    }
76
77    pub fn approval_config_handle(
78        &self,
79    ) -> Arc<parking_lot::RwLock<crate::approval::ApprovalConfig>> {
80        Arc::clone(&self.approval_config)
81    }
82    pub fn approval_config(&self) -> crate::approval::ApprovalConfig {
83        self.approval_config.read().clone()
84    }
85
86    pub async fn set_approval_config(
87        &self,
88        config: crate::approval::ApprovalConfig,
89    ) -> anyhow::Result<crate::approval::ApprovalConfig> {
90        *self.approval_config.write() = config.clone();
91        Ok(config)
92    }
93
94    pub async fn add_grant(&self, key: String) -> anyhow::Result<crate::approval::ApprovalConfig> {
95        let mut config = self.approval_config();
96        if !config.allow_list.contains(&key) {
97            config.allow_list.push(key);
98        }
99        self.set_approval_config(config.clone()).await
100    }
101
102    pub async fn remove_grant(&self, key: &str) -> anyhow::Result<crate::approval::ApprovalConfig> {
103        let mut config = self.approval_config();
104        config.allow_list.retain(|item| item != key);
105        self.set_approval_config(config.clone()).await
106    }
107
108    /// Get commit log.
109    pub fn git_log(&self, max: usize) -> anyhow::Result<Vec<LogEntry>> {
110        self.git_layer.log(max)
111    }
112
113    /// Tag current state.
114    pub fn git_tag(&self, name: &str, message: &str) -> anyhow::Result<()> {
115        self.git_layer.tag(name, message)
116    }
117
118    /// Restore file from commit.
119    pub fn git_restore(&self, path: &str, hash: &str) -> anyhow::Result<()> {
120        self.git_layer.restore_file(path, hash)
121    }
122
123    /// Verify git repository integrity.
124    pub fn git_verify(&self) -> anyhow::Result<bool> {
125        self.git_layer.verify()
126    }
127
128    /// List git tags.
129    pub fn git_tags(&self) -> anyhow::Result<Vec<String>> {
130        self.git_layer.list_tags()
131    }
132
133    /// Delete a tag by name.
134    pub fn git_delete_tag(&self, name: &str) -> anyhow::Result<()> {
135        self.git_layer.delete_tag(name)
136    }
137
138    /// Add a cron job.
139    pub async fn add_cron(&self, job: CronJob) -> anyhow::Result<uuid::Uuid> {
140        self.cron_scheduler.add_job(job).await
141    }
142
143    /// Get a cron job by ID.
144    pub fn get_cron(&self, id: uuid::Uuid) -> Option<CronJob> {
145        self.cron_scheduler.get_job(id)
146    }
147
148    /// Update a cron job.
149    pub async fn update_cron(&self, id: uuid::Uuid, update: CronJobUpdate) -> anyhow::Result<()> {
150        self.cron_scheduler.update_job(id, update).await
151    }
152
153    /// Remove a cron job by ID.
154    pub async fn remove_cron(&self, id: uuid::Uuid) -> anyhow::Result<()> {
155        self.cron_scheduler.remove_job(id).await
156    }
157
158    /// Trigger a cron job manually.
159    pub fn trigger_cron(&self, id: uuid::Uuid) -> anyhow::Result<CronJob> {
160        self.cron_scheduler.trigger_job(id)
161    }
162
163    /// Mark cron job completed.
164    pub async fn complete_cron(&self, id: uuid::Uuid, success: bool, summary: String) {
165        self.cron_scheduler
166            .mark_job_completed(id, success, summary)
167            .await
168    }
169
170    /// List all cron jobs.
171    pub fn list_crons(&self) -> Vec<CronJob> {
172        self.cron_scheduler.list_jobs()
173    }
174
175    /// Get resource snapshot.
176    pub fn resource_snapshot(&self) -> ResourceSnapshot {
177        self.resource_monitor.snapshot()
178    }
179
180    /// Get resource history snapshots.
181    pub fn resource_history(&self, last_n: usize) -> Vec<ResourceSnapshot> {
182        self.resource_monitor.history(last_n)
183    }
184
185    /// Check if system is overloaded.
186    pub fn is_overloaded(&self) -> bool {
187        self.resource_monitor.is_overloaded()
188    }
189
190    /// Subscribe to kernel events.
191    pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<KernelEvent> {
192        self.event_bus.subscribe()
193    }
194
195    /// Publish a kernel event.
196    pub fn publish(&self, event: KernelEvent) -> anyhow::Result<()> {
197        self.event_bus
198            .publish(event)
199            .map_err(|e| anyhow::anyhow!("broadcast error: {e}"))
200    }
201
202    /// Get config reference.
203    pub fn config(&self) -> &OxiosConfig {
204        &self.config
205    }
206
207    /// Resource monitor reference — for hot-reload config propagation.
208    pub fn resource_monitor(&self) -> &Arc<ResourceMonitor> {
209        &self.resource_monitor
210    }
211
212    /// Hot-reload orchestrator config (stored in InfraApi for propagation).
213    pub fn update_orchestrator_config(&self, config: crate::config::OrchestratorConfig) {
214        *self.orchestrator_config.write() = config;
215    }
216
217    /// Get system uptime.
218    pub fn uptime(&self) -> Duration {
219        self.start_time.elapsed()
220    }
221
222    /// Access the pending tool approvals registry.
223    pub fn pending_tool_approvals(&self) -> Arc<PendingToolApprovals> {
224        self.pending_tool_approvals.clone()
225    }
226    /// Access the pending path-access registry.
227    pub fn pending_path_access(&self) -> Arc<PendingPathAccess> {
228        self.pending_path_access.clone()
229    }
230
231    /// Access the pending `ask_user` registry (RFC-027 agent-driven clarification).
232    pub fn pending_ask_user(&self) -> Arc<PendingAskUser> {
233        Arc::clone(&self.pending_ask_user)
234    }
235
236    /// Clone the underlying event bus so tools can publish `KernelEvent`s
237    /// without routing through the wrapping `publish` helper.
238    pub fn event_bus_clone(&self) -> EventBus {
239        self.event_bus.clone()
240    }
241
242    /// List all known tool metadata (static catalog).
243    pub fn list_available_tools(&self) -> &'static [ToolMeta] {
244        known_tools()
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use crate::approval::{ApprovalConfig, ApprovalMode};
252    use crate::state_store::StateStore;
253
254    fn minimal_infra(approval_config: Arc<parking_lot::RwLock<ApprovalConfig>>) -> InfraApi {
255        let dir = tempfile::tempdir().unwrap();
256        InfraApi::new(
257            Arc::new(GitLayer::new(dir.path().join("git"), false).unwrap()),
258            Arc::new(CronScheduler::new(
259                Arc::new(StateStore::new(dir.path().join("state")).unwrap()),
260                60,
261            )),
262            Arc::new(ResourceMonitor::new(60, 60)),
263            EventBus::new(64),
264            OxiosConfig::default(),
265            std::time::Instant::now(),
266            Arc::new(PendingToolApprovals::new()),
267            Arc::new(PendingAskUser::new()),
268            approval_config,
269            Arc::new(PendingPathAccess::new()),
270        )
271    }
272
273    // Regression: InfraApi::new must store the PASSED approval_config Arc, not
274    // derive a fresh one from `config.security.approval`. Without sharing, the
275    // cached handle (HTTP PATCH /api/security/approval) and the preliminary
276    // handle (AgentRuntime's ApprovalGate) hold independent Arcs — a mode
277    // toggle writes one while the gate reads another, so AutoRun never takes
278    // effect and every OnDemand tool (web_search, exec, write …) re-prompts.
279    #[test]
280    fn new_stores_shared_approval_config_arc() {
281        let shared = Arc::new(parking_lot::RwLock::new(ApprovalConfig::default()));
282        let infra = minimal_infra(Arc::clone(&shared));
283        assert!(
284            Arc::ptr_eq(&infra.approval_config_handle(), &shared),
285            "InfraApi must store the passed approval_config Arc, not a fresh clone"
286        );
287    }
288
289    // Two InfraApi instances built from the same shared Arc must see each
290    // other's mutations — the cross-handle visibility contract.
291    #[tokio::test]
292    async fn shared_arc_visible_across_instances() {
293        let shared = Arc::new(parking_lot::RwLock::new(ApprovalConfig::default()));
294        let a = minimal_infra(Arc::clone(&shared));
295        let b = minimal_infra(shared);
296
297        // Simulates HTTP PATCH { mode: "auto-run" } on the cached handle.
298        let mut cfg = a.approval_config();
299        cfg.mode = ApprovalMode::AutoRun;
300        a.set_approval_config(cfg).await.unwrap();
301
302        // The AgentRuntime's gate (reading the preliminary handle) must see it.
303        assert_eq!(b.approval_config().mode, ApprovalMode::AutoRun);
304    }
305}