rig_harness/secrets.rs
1//! Secrets a plugin needs before any front runs, such as the passphrase of
2//! an encrypted session store, which opens before the session is restored.
3//!
4//! A plugin asks for one in `build` with `app.add_secret(..)`
5//! ([`AppCliExt`](crate::host::cli::AppCliExt)) and reads it from
6//! [`Secrets`], there from `build` on. The launcher learns what the build
7//! asks for when it describes it ([`describe`](crate::describe)) and,
8//! when stdin is a terminal, whatever the front, asks the user for each
9//! one the environment does not hold, once per launcher, whatever the
10//! restarts. A secret reaches the agent's process alone, as the variable
11//! [`rig_harness_protocol::secret_variable`] of its name
12//! (`RIG_SECRET_CRYPT`); a piped run gets only what the environment
13//! has. A secret not given is `None`, and the plugin decides what then.
14//!
15//! No process rig starts gets a secret: not cargo or any other launcher
16//! child, not `/reload`'s build, not a command started through
17//! [`Processes`](crate::services::Processes) (the shell and every tool), not
18//! the terminal view's editor. A plugin that starts a process with a bare
19//! `std::process::Command` passes its secrets on: start processes with
20//! `Processes`. What this protects from: tools, the model's commands,
21//! build scripts and proc macros, and helper programs. What it does not:
22//! the same user reading the agent's environment (`/proc/<pid>/environ`),
23//! and plugin code, which is trusted as all Bevy plugin code is.
24//!
25//! ```
26//! use rig_harness::prelude::*;
27//!
28//! #[derive(Default)]
29//! struct CryptPlugin;
30//!
31//! impl Plugin for CryptPlugin {
32//! fn build(&self, app: &mut App) {
33//! app.add_secret(Secret::new("crypt", "Passphrase for this machine's sessions"));
34//! let passphrase = app
35//! .world()
36//! .get_resource::<Secrets>()
37//! .and_then(|secrets| secrets.get("crypt"))
38//! .map(str::to_owned);
39//! // Open the session store with it, or refuse to start without.
40//! # drop(passphrase);
41//! }
42//! }
43//! ```
44
45use std::collections::BTreeMap;
46use std::fmt;
47
48use bevy_ecs::prelude::*;
49use rig_harness_protocol::{env, secret_variable};
50
51/// A secret a plugin asks for: its name, which names its variable, and
52/// what the launcher asks.
53#[derive(Component, Clone, Debug, PartialEq, Eq)]
54pub struct Secret {
55 /// Its name: lowercase letters, digits and dashes.
56 pub name: &'static str,
57 /// What to ask for, such as `Passphrase for this machine's sessions`.
58 pub help: &'static str,
59}
60
61impl Secret {
62 /// The secret `name`, asked for as `help`.
63 pub fn new(name: &'static str, help: &'static str) -> Self {
64 Self { name, help }
65 }
66}
67
68/// The secrets this agent was given. The harness takes them from the
69/// process's environment while it is built, unless the app inserted its
70/// own, as a test does: `Secrets::from_iter([("crypt", "passphrase")])`.
71/// Its `Debug` shows names only.
72#[derive(Resource, Clone, Default)]
73pub struct Secrets(BTreeMap<String, String>);
74
75impl Secrets {
76 /// Every secret in this process's environment.
77 pub fn from_env() -> Self {
78 Self(
79 std::env::vars_os()
80 .filter_map(|(name, value)| {
81 Some((name.into_string().ok()?, value.into_string().ok()?))
82 })
83 .filter(|(name, _)| name.starts_with(env::SECRET_PREFIX))
84 .collect(),
85 )
86 }
87
88 /// The secret `name`, if given.
89 pub fn get(&self, name: &str) -> Option<&str> {
90 self.0.get(&secret_variable(name)).map(String::as_str)
91 }
92}
93
94/// Secrets by name, such as a test's.
95impl<N: AsRef<str>, V: Into<String>> FromIterator<(N, V)> for Secrets {
96 fn from_iter<I: IntoIterator<Item = (N, V)>>(secrets: I) -> Self {
97 Self(
98 secrets
99 .into_iter()
100 .map(|(name, value)| (secret_variable(name.as_ref()), value.into()))
101 .collect(),
102 )
103 }
104}
105
106impl fmt::Debug for Secrets {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 f.debug_set().entries(self.0.keys()).finish()
109 }
110}