use serde::{Deserialize, Serialize};
use std::path::Path;
pub const READ_ONLY_DENIAL_MARKER: &str = "read-only safety mode";
pub const PLAN_DENIAL_MARKER: &str = "plan mode";
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SafetyMode {
Plan,
ReadOnly,
#[default]
Ask,
Auto,
FullAccess,
}
impl SafetyMode {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Plan => "plan",
Self::ReadOnly => "read_only",
Self::Ask => "ask",
Self::Auto => "auto",
Self::FullAccess => "full_access",
}
}
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
match s {
"plan" => Some(Self::Plan),
"read_only" => Some(Self::ReadOnly),
"ask" => Some(Self::Ask),
"auto" => Some(Self::Auto),
"full_access" => Some(Self::FullAccess),
_ => None,
}
}
#[must_use]
pub fn is_planning(self) -> bool {
matches!(self, Self::Plan)
}
#[must_use]
pub fn permissiveness(self) -> u8 {
match self {
Self::Plan => 0,
Self::ReadOnly => 1,
Self::Ask => 2,
Self::Auto => 3,
Self::FullAccess => 4,
}
}
#[must_use]
pub fn least_permissive(a: Self, b: Self) -> Self {
if a.permissiveness() <= b.permissiveness() {
a
} else {
b
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCategory {
Read,
Edit,
Shell,
Web,
ExternalDirectory,
ComputerUse,
Mcp,
Subagent,
Network,
Git,
Process,
Memory,
}
impl ToolCategory {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Read => "read",
Self::Memory => "memory",
Self::Edit => "edit",
Self::Shell => "shell",
Self::Web => "web",
Self::ExternalDirectory => "external_directory",
Self::ComputerUse => "computer_use",
Self::Mcp => "mcp",
Self::Subagent => "subagent",
Self::Network => "network",
Self::Git => "git",
Self::Process => "process",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RiskClass {
ReadOnly,
LowMutation,
FileMutation,
ShellMutation,
Network,
Process,
ExternalAccess,
SystemMutation,
Destructive,
}
impl RiskClass {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::ReadOnly => "read_only",
Self::LowMutation => "low_mutation",
Self::FileMutation => "file_mutation",
Self::ShellMutation => "shell_mutation",
Self::Network => "network",
Self::Process => "process",
Self::ExternalAccess => "external_access",
Self::SystemMutation => "system_mutation",
Self::Destructive => "destructive",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActionRequest {
pub tool: String,
pub category: ToolCategory,
pub summary: String,
pub command: Option<String>,
pub path: Option<String>,
pub arguments: Option<serde_json::Value>,
pub mcp_read_only_hint: bool,
pub cwd: Option<std::path::PathBuf>,
}
impl ActionRequest {
pub fn new(
tool: impl Into<String>,
category: ToolCategory,
summary: impl Into<String>,
) -> Self {
Self {
tool: tool.into(),
category,
summary: summary.into(),
command: None,
path: None,
arguments: None,
mcp_read_only_hint: false,
cwd: None,
}
}
#[must_use]
pub fn resolve_dir<'a>(&'a self, fallback: &'a Path) -> &'a Path {
self.cwd.as_deref().unwrap_or(fallback)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyDecision {
Allow {
risk: RiskClass,
checkpoint: bool,
},
Ask {
risk: RiskClass,
checkpoint: bool,
},
Classify {
risk: RiskClass,
checkpoint: bool,
},
Deny {
risk: RiskClass,
reason: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyOverrideDecision {
Allow,
Ask,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct PolicyOverride {
pub category: Option<ToolCategory>,
pub tool: Option<String>,
pub pattern: Option<String>,
pub decision: PolicyOverrideDecision,
pub checkpoint: Option<bool>,
pub reason: Option<String>,
}
impl Default for PolicyOverride {
fn default() -> Self {
Self {
category: None,
tool: None,
pattern: None,
decision: PolicyOverrideDecision::Ask,
checkpoint: None,
reason: None,
}
}
}
impl PolicyDecision {
#[must_use]
pub fn risk(&self) -> RiskClass {
match self {
Self::Allow { risk, .. }
| Self::Ask { risk, .. }
| Self::Classify { risk, .. }
| Self::Deny { risk, .. } => *risk,
}
}
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::Allow { .. } => "allow",
Self::Ask { .. } => "ask",
Self::Classify { .. } => "classify",
Self::Deny { .. } => "deny",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FloorLevel {
Allow,
#[default]
Auto,
Ask,
Deny,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostShell {
Posix,
PowerShell,
}
impl HostShell {
#[must_use]
pub const fn current() -> Self {
if cfg!(target_os = "windows") {
Self::PowerShell
} else {
Self::Posix
}
}
#[must_use]
pub const fn display_name(self) -> &'static str {
match self {
Self::Posix => "Bash",
Self::PowerShell => "PowerShell",
}
}
#[must_use]
pub const fn prompt_sigil(self) -> &'static str {
match self {
Self::Posix => "$ ",
Self::PowerShell => "PS> ",
}
}
}
#[cfg(test)]
mod tests {
use super::SafetyMode;
#[test]
fn least_permissive_picks_the_stricter_mode() {
use SafetyMode::*;
assert_eq!(SafetyMode::least_permissive(FullAccess, ReadOnly), ReadOnly);
assert_eq!(SafetyMode::least_permissive(ReadOnly, FullAccess), ReadOnly);
assert_eq!(SafetyMode::least_permissive(Ask, Auto), Ask);
assert_eq!(SafetyMode::least_permissive(Auto, Ask), Ask);
for m in [ReadOnly, Ask, Auto, FullAccess] {
assert_eq!(SafetyMode::least_permissive(m, m), m);
}
for m in [ReadOnly, Ask, Auto, FullAccess] {
assert_eq!(SafetyMode::least_permissive(m, FullAccess), m);
}
}
}