Skip to main content

rig_harness/
services.rs

1//! What every plugin shares about the process it runs in, as resources:
2//! the `RIG_HOME` layout ([`RigHome`]), the project the session works on
3//! ([`ProjectRoot`]) and the directory an agent works in when not that one
4//! ([`WorkDir`]), how child processes start ([`Processes`]), and the
5//! tokio runtime rig's HTTP requests run on ([`TokioRuntime`]). Each is
6//! taken from the environment unless the app inserted one first, which is
7//! how a test points a plugin at a directory of its own; a plugin that
8//! should also work in an app without the harness's plugins inits the
9//! ones it reads (`app.init_resource::<ProjectRoot>()`).
10//!
11//! ```
12//! use rig_harness::prelude::*;
13//!
14//! let mut app = App::new();
15//! app.insert_resource(ProjectRoot("/srv/project".into()))
16//!     .add_plugins(rig_harness::services::ServicesPlugin);
17//! let root = &app.world().resource::<ProjectRoot>().0;
18//! assert_eq!(root, std::path::Path::new("/srv/project"));
19//! ```
20
21use std::ffi::OsString;
22use std::path::PathBuf;
23use std::sync::{Arc, PoisonError, RwLock};
24
25use bevy_app::prelude::*;
26use bevy_ecs::prelude::*;
27use bevy_log::warn;
28use bevy_reflect::prelude::*;
29use rig_ecs::agent::SpawnedBy;
30use rig_ecs::session_log::ReflectSaved;
31use rig_harness_protocol::{Home, env, secret_variables};
32use rig_tools::process::Launcher;
33
34/// The `RIG_HOME` this agent runs under: where its sessions, settings,
35/// sign-ins and builds are.
36#[derive(Resource, Clone, Debug)]
37pub struct RigHome(pub Home);
38
39impl RigHome {
40    /// The app's home, else the one the environment names.
41    pub fn of(world: &World) -> Home {
42        world
43            .get_resource::<Self>()
44            .map_or_else(Home::from_env, |home| home.0.clone())
45    }
46}
47
48/// The one the environment names.
49impl Default for RigHome {
50    fn default() -> Self {
51        Self(Home::from_env())
52    }
53}
54
55/// The directory the session works in: where it was first started, which
56/// the launcher starts a resumed session in again, and sets the agent's
57/// working directory to. Paths the user and the model name are relative to
58/// it: a file tool of an agent without a [`WorkDir`] resolves them from
59/// here, and a command starts here, in the app's
60/// [`ProjectWorkspace`](crate::workspace::ProjectWorkspace)
61/// (`rig_basics::coding_tools`).
62#[derive(Resource, Clone, Debug, PartialEq, Eq)]
63pub struct ProjectRoot(pub PathBuf);
64
65/// This process's working directory, which the launcher sets to the
66/// session's.
67impl Default for ProjectRoot {
68    fn default() -> Self {
69        Self(std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
70    }
71}
72
73/// On an agent: the directory it works in, such as a worktree of its own,
74/// in place of the [`ProjectRoot`]. Its file tools resolve relative paths
75/// from it and refuse a path outside it, and its commands start in it
76/// (`rig_basics::coding_tools`); what a command does once started is not confined.
77/// Saved with the agent. An agent it spawns works there too, unless given
78/// one of its own.
79#[derive(Component, Reflect, Clone, Debug, PartialEq, Eq)]
80#[reflect(Component, Saved, Clone, Debug, PartialEq)]
81pub struct WorkDir(pub PathBuf);
82
83/// How every child process starts: under which wrapper, in which
84/// directory, with which environment. The shell tool and any plugin that
85/// starts a process build each command from it when the process starts
86/// (`processes.launcher().command(program)`), so a sandbox plugin that
87/// changes it ([`Processes::set`]), in `build` or while the app runs,
88/// changes how the next process starts. By default no command inherits
89/// what makes a program act as this agent ([`env::AGENT_ONLY`]), and no
90/// command ever inherits a secret
91/// ([`secrets`](crate::secrets)), whatever launcher is set.
92#[derive(Resource, Clone, Debug)]
93pub struct Processes {
94    launcher: Arc<RwLock<Launcher>>,
95    /// The secret variables of this process, which no command inherits.
96    secrets: Arc<[OsString]>,
97}
98
99impl Processes {
100    /// How the next process starts: the launcher set, keeping every secret
101    /// from the command.
102    pub fn launcher(&self) -> Launcher {
103        let launcher = self
104            .launcher
105            .read()
106            .unwrap_or_else(PoisonError::into_inner)
107            .clone();
108        let kept: Vec<OsString> = self
109            .secrets
110            .iter()
111            .filter(|secret| !launcher.unset_env.contains(secret))
112            .cloned()
113            .collect();
114        launcher.without_env(kept)
115    }
116
117    /// Starts every process from now on as `launcher` says, without the
118    /// secrets.
119    pub fn set(&self, launcher: Launcher) {
120        *self
121            .launcher
122            .write()
123            .unwrap_or_else(PoisonError::into_inner) = launcher;
124    }
125}
126
127impl Default for Processes {
128    fn default() -> Self {
129        let launcher = Launcher::default().without_env(env::AGENT_ONLY);
130        Self {
131            launcher: Arc::new(RwLock::new(launcher)),
132            secrets: secret_variables().into(),
133        }
134    }
135}
136
137/// The tokio runtime rig's HTTP requests run on, for a plugin whose
138/// library needs tokio (an MCP client, a web server): one runtime per
139/// process instead of one per plugin.
140#[derive(Resource, Clone, Debug)]
141pub struct TokioRuntime(pub tokio::runtime::Handle);
142
143/// Inserts [`RigHome`], [`ProjectRoot`], [`Processes`] and
144/// [`TokioRuntime`], each unless the app has it, and gives an agent
145/// spawned by one with a [`WorkDir`] that directory.
146pub struct ServicesPlugin;
147
148impl Plugin for ServicesPlugin {
149    fn build(&self, app: &mut App) {
150        if !app.world().contains_resource::<TokioRuntime>() {
151            match rig_http::runtime::handle() {
152                Ok(handle) => {
153                    app.insert_resource(TokioRuntime(handle));
154                }
155                Err(failure) => warn!("no tokio runtime for plugins: {failure}"),
156            }
157        }
158        app.init_resource::<RigHome>()
159            .init_resource::<ProjectRoot>()
160            .init_resource::<Processes>()
161            .add_observer(inherit_work_dir);
162    }
163}
164
165/// Gives a spawned agent its parent's [`WorkDir`], unless it has one.
166fn inherit_work_dir(
167    spawned: On<Add<SpawnedBy>>,
168    parents: Query<&SpawnedBy>,
169    dirs: Query<&WorkDir>,
170    mut commands: Commands,
171) {
172    let parent = parents.get(spawned.entity).map(|&SpawnedBy(parent)| parent);
173    if let Some(dir) = parent.ok().and_then(|parent| dirs.get(parent).ok()) {
174        commands
175            .entity(spawned.entity)
176            .try_insert_if_new(dir.clone());
177    }
178}
179
180#[cfg(test)]
181mod tests;