use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::RiskTier;
pub const BASH_TOOL: &str = "bash";
pub const BASH_WAIVER_MAX: RiskTier = RiskTier::Destructive;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FloorWaiver {
#[default]
Off,
All,
UpTo(RiskTier),
}
impl FloorWaiver {
pub fn is_off(&self) -> bool {
*self == FloorWaiver::Off
}
pub fn covers(&self, tier: RiskTier) -> bool {
match self {
FloorWaiver::Off => false,
FloorWaiver::All => true,
FloorWaiver::UpTo(max) => tier <= *max,
}
}
}
impl Serialize for FloorWaiver {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
match self {
FloorWaiver::Off => s.serialize_bool(false),
FloorWaiver::All => s.serialize_bool(true),
FloorWaiver::UpTo(t) => t.serialize(s),
}
}
}
impl<'de> Deserialize<'de> for FloorWaiver {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Bool(bool),
Tier(RiskTier),
}
Ok(
match Raw::deserialize(d).map_err(|_| {
serde::de::Error::custom(
"bypass_builtin_tier_floor: expected true/false or a tier \
(write, network-egress, spend, destructive)",
)
})? {
Raw::Bool(false) => FloorWaiver::Off,
Raw::Bool(true) => FloorWaiver::All,
Raw::Tier(t) => FloorWaiver::UpTo(t),
},
)
}
}
pub fn bash_ceiling(rule: Option<&crate::agent::ToolRule>) -> RiskTier {
let base = RiskTier::Write;
match rule {
Some(r) if r.pattern == BASH_TOOL => match r.bypass_builtin_tier_floor {
FloorWaiver::UpTo(t) => t.min(BASH_WAIVER_MAX).max(base),
FloorWaiver::Off | FloorWaiver::All => base,
},
_ => base,
}
}
pub fn waiver_warnings(rules: &[crate::agent::ToolRule]) -> Vec<String> {
let mut out = Vec::new();
for r in rules.iter().filter(|r| r.pattern == BASH_TOOL) {
match r.bypass_builtin_tier_floor {
FloorWaiver::All => out.push(format!(
"profile.yaml tool rule `{BASH_TOOL}`: bypass_builtin_tier_floor: true is IGNORED \
(it would waive every command); name a ceiling instead, e.g. network-egress (#1814)"
)),
FloorWaiver::UpTo(RiskTier::Privileged) => out.push(format!(
"profile.yaml tool rule `{BASH_TOOL}`: bypass_builtin_tier_floor: privileged is capped \
at destructive — commands tiered privileged still ask (#1814)"
)),
_ => {}
}
}
out
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CeilingHit {
pub tier: RiskTier,
pub ceiling: RiskTier,
pub rule: Option<String>,
}
pub fn bash_ceiling_hit(
rules: &[crate::agent::ToolRule],
tool_name: &str,
input: Option<&serde_json::Value>,
) -> Option<CeilingHit> {
if tool_name != BASH_TOOL {
return None;
}
let rule = crate::agent::resolve_tool_rule(rules, BASH_TOOL);
let ceiling = bash_ceiling(rule);
let tier = super::tool_tier::classify(BASH_TOOL, input);
(tier > ceiling).then(|| CeilingHit {
tier,
ceiling,
rule: rule.map(|r| r.pattern.clone()),
})
}
#[cfg(test)]
mod tests;