use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
pub const TRUST_VERSION: u32 = 1;
pub const COMMAND_FILES: [&str; 2] = ["agent.ron", "play.ron"];
pub const STORE_FILE: &str = "trusted.ron";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CommandKind {
Notes,
Play,
}
impl CommandKind {
pub fn noun(self) -> &'static str {
match self {
CommandKind::Notes => "notes agent",
CommandKind::Play => "game",
}
}
pub fn verb(self) -> &'static str {
match self {
CommandKind::Notes => "Send",
CommandKind::Play => "Play",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommandSpec {
pub kind: CommandKind,
pub program: String,
pub args: Vec<String>,
pub cwd: Option<String>,
}
impl CommandSpec {
pub fn display(&self) -> String {
let mut words = vec![self.program.as_str()];
words.extend(self.args.iter().map(String::as_str));
let line = words.join(" ");
match &self.cwd {
Some(dir) => format!("{line} (in {dir})"),
None => line,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Scope {
Command,
Project,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Decision {
Trusted(Scope),
Untrusted,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Grant {
project: PathBuf,
command: CommandSpec,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TrustStore {
version: u32,
projects: Vec<PathBuf>,
commands: Vec<Grant>,
}
impl Default for TrustStore {
fn default() -> Self {
TrustStore { version: TRUST_VERSION, projects: Vec::new(), commands: Vec::new() }
}
}
fn resolve(root: &Path) -> Result<PathBuf, String> {
std::fs::canonicalize(root)
.map_err(|e| format!("the project folder {} cannot be resolved, so nothing about it can be trusted: {e}", root.display()))
}
impl TrustStore {
pub fn load(file: &Path) -> Result<TrustStore, String> {
let text = match std::fs::read_to_string(file) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(TrustStore::default()),
Err(e) => return Err(format!("trust: {}: {e}", file.display())),
};
let store: TrustStore =
ron::from_str(&text).map_err(|e| format!("trust: {} does not read ({e}). Fix or delete it: nothing is trusted until it does.", file.display()))?;
if store.version != TRUST_VERSION {
return Err(format!(
"trust: {} is version {}, and this build reads version {TRUST_VERSION}. Nothing is trusted until it does.",
file.display(),
store.version
));
}
Ok(store)
}
pub fn save(&self, file: &Path) -> Result<(), String> {
let text = ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default())
.map_err(|e| format!("trust: cannot write {}: {e}", file.display()))?;
crate::atomic_file::write(file, text).map_err(|e| format!("trust: {}: {e}", file.display()))
}
pub fn decision(&self, root: &Path, command: &CommandSpec) -> Result<Decision, String> {
let root = resolve(root)?;
if self.projects.contains(&root) {
return Ok(Decision::Trusted(Scope::Project));
}
if self.commands.iter().any(|g| g.project == root && g.command == *command) {
return Ok(Decision::Trusted(Scope::Command));
}
Ok(Decision::Untrusted)
}
pub fn trust_project(&mut self, root: &Path) -> Result<(), String> {
let root = resolve(root)?;
if !self.projects.contains(&root) {
self.projects.push(root);
}
Ok(())
}
pub fn trust_command(&mut self, root: &Path, command: &CommandSpec) -> Result<(), String> {
let project = resolve(root)?;
let grant = Grant { project, command: command.clone() };
if !self.commands.contains(&grant) {
self.commands.push(grant);
}
Ok(())
}
pub fn untrusted_sentence(root: &Path, command: &CommandSpec) -> String {
format!(
"{} wants to start the {} `{}`, and no person has allowed that. Nothing was started. A person can allow this command or the whole project in the editor; an agent cannot.",
root.display(),
command.kind.noun(),
command.display()
)
}
}