use serde::{Deserialize, Serialize};
use std::str::FromStr;
use crate::decision::{PolicyDecision, PolicyDecisionKind};
pub const RESPONSE_LADDER_ANCHOR: &str = "deepmind-ai-control-roadmap-r1-r3";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum Reversibility {
Reversible,
Costly,
#[default]
Irreversible,
}
impl Reversibility {
pub fn as_str(self) -> &'static str {
match self {
Reversibility::Reversible => "reversible",
Reversibility::Costly => "costly",
Reversibility::Irreversible => "irreversible",
}
}
pub fn parse(value: &str) -> Self {
match value.trim().to_lowercase().as_str() {
"reversible" => Reversibility::Reversible,
"costly" => Reversibility::Costly,
_ => Reversibility::Irreversible,
}
}
}
impl FromStr for Reversibility {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Reversibility::parse(s))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResponseLevel {
AllowAndLog,
AllowUnderBudget,
RequireApproval,
}
impl ResponseLevel {
pub fn as_str(self) -> &'static str {
match self {
ResponseLevel::AllowAndLog => "allow_and_log",
ResponseLevel::AllowUnderBudget => "allow_under_budget",
ResponseLevel::RequireApproval => "require_approval",
}
}
pub fn rung(self) -> &'static str {
match self {
ResponseLevel::AllowAndLog => "R1",
ResponseLevel::AllowUnderBudget => "R2",
ResponseLevel::RequireApproval => "R3",
}
}
pub fn decision_kind(self) -> PolicyDecisionKind {
match self {
ResponseLevel::AllowAndLog | ResponseLevel::AllowUnderBudget => {
PolicyDecisionKind::Allow
}
ResponseLevel::RequireApproval => PolicyDecisionKind::RequiresApproval,
}
}
pub fn to_policy_decision(self, reason: impl Into<String>) -> PolicyDecision {
match self {
ResponseLevel::AllowAndLog | ResponseLevel::AllowUnderBudget => {
PolicyDecision::allow(reason)
}
ResponseLevel::RequireApproval => PolicyDecision::requires_approval(reason),
}
}
}
pub fn graduated_response(rev: Reversibility, budget_has_headroom: bool) -> ResponseLevel {
match rev {
Reversibility::Reversible => ResponseLevel::AllowAndLog,
Reversibility::Costly => {
if budget_has_headroom {
ResponseLevel::AllowUnderBudget
} else {
ResponseLevel::RequireApproval
}
}
Reversibility::Irreversible => ResponseLevel::RequireApproval,
}
}
fn restrictiveness(kind: PolicyDecisionKind) -> u8 {
match kind {
PolicyDecisionKind::Deny => 0,
PolicyDecisionKind::RequiresApproval => 1,
PolicyDecisionKind::AllowWithWarning => 2,
PolicyDecisionKind::Allow => 3,
}
}
pub fn combine(base: PolicyDecisionKind, level: ResponseLevel) -> PolicyDecisionKind {
let candidate = level.decision_kind();
if restrictiveness(candidate) < restrictiveness(base) {
candidate
} else {
base
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_irreversible() {
assert_eq!(Reversibility::default(), Reversibility::Irreversible);
}
#[test]
fn parse_unknown_defaults_to_irreversible() {
assert_eq!(
Reversibility::parse("reversible"),
Reversibility::Reversible
);
assert_eq!(Reversibility::parse("COSTLY"), Reversibility::Costly);
assert_eq!(
Reversibility::parse("irreversible"),
Reversibility::Irreversible
);
assert_eq!(
Reversibility::parse("nonsense"),
Reversibility::Irreversible
);
assert_eq!(Reversibility::parse(""), Reversibility::Irreversible);
assert_eq!(
"garbage".parse::<Reversibility>().unwrap(),
Reversibility::Irreversible
);
}
#[test]
fn ladder_reversible_is_r1_allow_and_log() {
assert_eq!(
graduated_response(Reversibility::Reversible, true),
ResponseLevel::AllowAndLog
);
assert_eq!(
graduated_response(Reversibility::Reversible, false),
ResponseLevel::AllowAndLog
);
}
#[test]
fn ladder_irreversible_is_r3_require_approval() {
assert_eq!(
graduated_response(Reversibility::Irreversible, true),
ResponseLevel::RequireApproval
);
}
#[test]
fn ladder_costly_flips_to_approval_when_budget_exhausted() {
assert_eq!(
graduated_response(Reversibility::Costly, true),
ResponseLevel::AllowUnderBudget
);
assert_eq!(
graduated_response(Reversibility::Costly, false),
ResponseLevel::RequireApproval
);
}
#[test]
fn response_level_wire_and_rung_labels() {
assert_eq!(ResponseLevel::AllowAndLog.as_str(), "allow_and_log");
assert_eq!(
ResponseLevel::AllowUnderBudget.as_str(),
"allow_under_budget"
);
assert_eq!(ResponseLevel::RequireApproval.as_str(), "require_approval");
assert_eq!(ResponseLevel::AllowAndLog.rung(), "R1");
assert_eq!(ResponseLevel::AllowUnderBudget.rung(), "R2");
assert_eq!(ResponseLevel::RequireApproval.rung(), "R3");
}
#[test]
fn to_policy_decision_maps_rungs() {
assert!(ResponseLevel::AllowAndLog
.to_policy_decision("r1")
.is_allowed());
assert!(ResponseLevel::AllowUnderBudget
.to_policy_decision("r2")
.is_allowed());
assert!(ResponseLevel::RequireApproval
.to_policy_decision("r3")
.needs_approval());
}
#[test]
fn combine_adds_friction_but_never_loosens() {
assert_eq!(
combine(PolicyDecisionKind::Allow, ResponseLevel::RequireApproval),
PolicyDecisionKind::RequiresApproval
);
assert_eq!(
combine(PolicyDecisionKind::Deny, ResponseLevel::AllowAndLog),
PolicyDecisionKind::Deny
);
assert_eq!(
combine(
PolicyDecisionKind::RequiresApproval,
ResponseLevel::AllowAndLog
),
PolicyDecisionKind::RequiresApproval
);
assert_eq!(
combine(PolicyDecisionKind::Allow, ResponseLevel::AllowUnderBudget),
PolicyDecisionKind::Allow
);
}
#[test]
fn serde_snake_case_round_trip() {
for r in [
Reversibility::Reversible,
Reversibility::Costly,
Reversibility::Irreversible,
] {
let json = serde_json::to_string(&r).unwrap();
assert_eq!(json, format!("\"{}\"", r.as_str()));
let back: Reversibility = serde_json::from_str(&json).unwrap();
assert_eq!(back, r);
}
}
}