Skip to main content

atman_runtime/
trust.rs

1/// Trust mode controls how aggressively tools auto-approve.
2#[derive(
3    Debug,
4    Clone,
5    Copy,
6    PartialEq,
7    Eq,
8    Default,
9    serde::Serialize,
10    serde::Deserialize,
11    documented::DocumentedVariants,
12)]
13#[serde(rename_all = "lowercase")]
14pub enum TrustMode {
15    /// Auto-approve only Tier::Zero (read-only) tools; everything else needs manual approval.
16    Calm,
17    #[default]
18    /// Auto-approve Tier::Zero and Tier::One; Tier::Two+ needs approval.
19    Steady,
20    /// Auto-approve up to Tier::Two; Tier::Three+ needs approval.
21    Eager,
22    /// Auto-approve everything including dangerous operations.
23    Reckless,
24}
25
26impl TrustMode {
27    pub fn sandbox_enabled(self) -> bool {
28        !matches!(self, Self::Reckless)
29    }
30
31    pub fn level(self) -> u8 {
32        match self {
33            Self::Calm => 1,
34            Self::Steady => 2,
35            Self::Eager => 3,
36            Self::Reckless => 4,
37        }
38    }
39
40    pub fn needs_warning(self) -> bool {
41        matches!(self, Self::Eager | Self::Reckless)
42    }
43
44    pub fn warning(self, display: &ModeDisplay) -> Option<String> {
45        match self {
46            Self::Eager => Some(format!(
47                "⚠ {} mode: sandbox guards bash/fs. Workspace-internal ops auto-approved. \
48                 Network is unrestricted. Escalated risks follow the configured \
49                 escalation policy (deny / ask / allow).",
50                display.name
51            )),
52            Self::Reckless => Some(format!(
53                "⚠ {} mode: sandbox is off. The agent can read/write outside the workspace, \
54                 run arbitrary commands, and access the network — all without confirmation. \
55                 Make sure you understand the risk.\n\n\
56                 Recommended only for one-off / sandbox projects, not for production repos \
57                 or directories with sensitive data.",
58                display.name
59            )),
60            _ => None,
61        }
62    }
63
64    pub fn all() -> [TrustMode; 4] {
65        [Self::Calm, Self::Steady, Self::Eager, Self::Reckless]
66    }
67}
68
69impl std::str::FromStr for TrustMode {
70    type Err = String;
71    fn from_str(s: &str) -> Result<Self, Self::Err> {
72        match s.to_ascii_lowercase().as_str() {
73            "calm" | "1" => Ok(Self::Calm),
74            "steady" | "2" | "default" => Ok(Self::Steady),
75            "eager" | "3" => Ok(Self::Eager),
76            "reckless" | "yolo" | "4" => Ok(Self::Reckless),
77            other => Err(format!("unknown trust mode `{other}`")),
78        }
79    }
80}
81
82/// The action selected by controlled permission policy.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
84#[serde(rename_all = "lowercase")]
85pub enum PolicyAction {
86    Auto,
87    Ask,
88    Deny,
89}
90
91impl PolicyAction {
92    pub fn most_restrictive(self, other: Self) -> Self {
93        match (self, other) {
94            (Self::Deny, _) | (_, Self::Deny) => Self::Deny,
95            (Self::Ask, _) | (_, Self::Ask) => Self::Ask,
96            (Self::Auto, Self::Auto) => Self::Auto,
97        }
98    }
99}
100
101/// How Eager mode handles risks that require escalation.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
103#[serde(rename_all = "lowercase")]
104pub enum EscalationPolicy {
105    Deny,
106    #[default]
107    Ask,
108    Allow,
109}
110
111/// How Eager transformed an otherwise-Ask policy result.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum PolicyEscalation {
114    None,
115    Denied,
116    Pending,
117    Allowed,
118}
119
120/// Policy result retaining whether an automatic decision came from Eager Allow.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct PolicyResolution {
123    pub action: PolicyAction,
124    pub escalation: PolicyEscalation,
125}
126
127impl EscalationPolicy {
128    pub fn next(self) -> Self {
129        match self {
130            Self::Deny => Self::Ask,
131            Self::Ask => Self::Allow,
132            Self::Allow => Self::Deny,
133        }
134    }
135
136    pub fn label(self) -> &'static str {
137        match self {
138            Self::Deny => "deny",
139            Self::Ask => "ask",
140            Self::Allow => "allow",
141        }
142    }
143}
144
145/// Whether Atman's permission controls apply to an execution.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum ExecutionPolicy {
148    Controlled,
149    Unrestricted,
150}
151
152/// Structured resource risks combined with a tool's Tier policy.
153#[derive(
154    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
155)]
156#[serde(rename_all = "snake_case")]
157pub enum RiskKind {
158    WorkspaceExternal,
159    Network,
160    Irreversible,
161    FilesystemWrite,
162    ProcessSpawn,
163    RepositoryMutation,
164}
165
166/// Optional Eager-mode overrides for each tool Tier.
167#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
168pub struct TierPolicyOverrides {
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub tier0: Option<PolicyAction>,
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub tier1: Option<PolicyAction>,
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub tier2: Option<PolicyAction>,
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub tier3: Option<PolicyAction>,
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub tier4: Option<PolicyAction>,
179}
180
181impl TierPolicyOverrides {
182    fn resolve(&self, tier: crate::tool::Tier) -> PolicyAction {
183        let configured = match tier {
184            crate::tool::Tier::Zero => self.tier0,
185            crate::tool::Tier::One => self.tier1,
186            crate::tool::Tier::Two => self.tier2,
187            crate::tool::Tier::Three => self.tier3,
188            crate::tool::Tier::Four => self.tier4,
189        };
190        configured.unwrap_or_else(|| default_eager_tier_action(tier))
191    }
192}
193
194/// Mode-specific Tier overrides. Calm and Steady intentionally have no configurable entries.
195#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
196pub struct TierPolicyConfig {
197    #[serde(default)]
198    pub eager: TierPolicyOverrides,
199}
200
201/// Optional Eager-mode overrides for structured resource risks.
202#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
203#[serde(deny_unknown_fields)]
204pub struct RiskPolicyOverrides {
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    pub outside_workspace: Option<PolicyAction>,
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub network: Option<PolicyAction>,
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub irreversible: Option<PolicyAction>,
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub filesystem_write: Option<PolicyAction>,
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub process_spawn: Option<PolicyAction>,
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    pub repository_mutation: Option<PolicyAction>,
217}
218
219impl RiskPolicyOverrides {
220    fn resolve(&self, risk: RiskKind) -> PolicyAction {
221        match risk {
222            RiskKind::WorkspaceExternal => self.outside_workspace,
223            RiskKind::Network => self.network,
224            RiskKind::Irreversible => self.irreversible,
225            RiskKind::FilesystemWrite => self.filesystem_write,
226            RiskKind::ProcessSpawn => self.process_spawn,
227            RiskKind::RepositoryMutation => self.repository_mutation,
228        }
229        .unwrap_or(PolicyAction::Ask)
230    }
231}
232
233/// Mode-specific risk overrides. Calm and Steady retain fixed safety floors.
234#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
235#[serde(deny_unknown_fields)]
236pub struct RiskPolicyConfig {
237    #[serde(default)]
238    pub eager: RiskPolicyOverrides,
239}
240
241fn default_eager_tier_action(tier: crate::tool::Tier) -> PolicyAction {
242    match tier {
243        crate::tool::Tier::Zero | crate::tool::Tier::One => PolicyAction::Auto,
244        crate::tool::Tier::Two | crate::tool::Tier::Three | crate::tool::Tier::Four => {
245            PolicyAction::Ask
246        }
247    }
248}
249
250/// Display theme for trust mode labels in the TUI.
251#[derive(
252    Debug,
253    Clone,
254    Copy,
255    PartialEq,
256    Eq,
257    serde::Serialize,
258    serde::Deserialize,
259    Default,
260    documented::DocumentedVariants,
261)]
262#[serde(rename_all = "lowercase")]
263pub enum Theme {
264    #[default]
265    /// Standard English labels.
266    Default,
267    /// Wuxia (martial arts) themed labels.
268    Wuxia,
269    /// Animal-themed labels.
270    Animal,
271    /// Weather-themed labels.
272    Weather,
273    /// Drink-themed labels.
274    Drink,
275}
276
277impl std::str::FromStr for Theme {
278    type Err = String;
279    fn from_str(s: &str) -> Result<Self, Self::Err> {
280        match s.to_ascii_lowercase().as_str() {
281            "default" => Ok(Self::Default),
282            "wuxia" => Ok(Self::Wuxia),
283            "animal" => Ok(Self::Animal),
284            "weather" => Ok(Self::Weather),
285            "drink" => Ok(Self::Drink),
286            other => Err(format!("unknown theme `{other}`")),
287        }
288    }
289}
290
291impl std::fmt::Display for Theme {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        match self {
294            Self::Default => write!(f, "default"),
295            Self::Wuxia => write!(f, "wuxia"),
296            Self::Animal => write!(f, "animal"),
297            Self::Weather => write!(f, "weather"),
298            Self::Drink => write!(f, "drink"),
299        }
300    }
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub enum ModeColor {
305    Cyan,
306    Green,
307    Yellow,
308    Red,
309    Orange,
310}
311
312#[derive(Debug, Clone)]
313pub struct ModeDisplay {
314    pub name: &'static str,
315    pub emoji: &'static str,
316    pub color: ModeColor,
317    pub description: &'static str,
318}
319
320#[derive(Debug, Clone)]
321pub struct EscalationDisplay {
322    pub name: &'static str,
323    pub emoji: &'static str,
324    pub color: ModeColor,
325}
326
327impl Theme {
328    pub fn display(&self, mode: TrustMode) -> ModeDisplay {
329        match (self, mode) {
330            (Theme::Default, TrustMode::Calm) => ModeDisplay {
331                name: "calm",
332                emoji: "🌙",
333                color: ModeColor::Cyan,
334                description: "confirm every step",
335            },
336            (Theme::Default, TrustMode::Steady) => ModeDisplay {
337                name: "steady",
338                emoji: "✓",
339                color: ModeColor::Green,
340                description: "auto low-risk work, confirm escalation",
341            },
342            (Theme::Default, TrustMode::Eager) => ModeDisplay {
343                name: "eager",
344                emoji: "⚡",
345                color: ModeColor::Yellow,
346                description: "auto routine work, escalation policy controls risk",
347            },
348            (Theme::Default, TrustMode::Reckless) => ModeDisplay {
349                name: "reckless",
350                emoji: "🔥",
351                color: ModeColor::Red,
352                description: "all off, you decide",
353            },
354
355            (Theme::Wuxia, TrustMode::Calm) => ModeDisplay {
356                name: "守拙",
357                emoji: "🧘",
358                color: ModeColor::Cyan,
359                description: "大巧若拙,步步为营",
360            },
361            (Theme::Wuxia, TrustMode::Steady) => ModeDisplay {
362                name: "行云",
363                emoji: "☁️",
364                color: ModeColor::Green,
365                description: "行云流水,任意所至",
366            },
367            (Theme::Wuxia, TrustMode::Eager) => ModeDisplay {
368                name: "破竹",
369                emoji: "🎋",
370                color: ModeColor::Yellow,
371                description: "势如破竹,迎刃而解",
372            },
373            (Theme::Wuxia, TrustMode::Reckless) => ModeDisplay {
374                name: "逍遥",
375                emoji: "🕊️",
376                color: ModeColor::Red,
377                description: "逍遥御风,无招胜有招",
378            },
379
380            (Theme::Animal, TrustMode::Calm) => ModeDisplay {
381                name: "hedgehog",
382                emoji: "🦔",
383                color: ModeColor::Cyan,
384                description: "curls up, asks about everything",
385            },
386            (Theme::Animal, TrustMode::Steady) => ModeDisplay {
387                name: "cat",
388                emoji: "🐱",
389                color: ModeColor::Green,
390                description: "roams its territory, wary of strangers",
391            },
392            (Theme::Animal, TrustMode::Eager) => ModeDisplay {
393                name: "dog",
394                emoji: "🐶",
395                color: ModeColor::Yellow,
396                description: "fence guards, charges ahead",
397            },
398            (Theme::Animal, TrustMode::Reckless) => ModeDisplay {
399                name: "honey-badger",
400                emoji: "🦡",
401                color: ModeColor::Red,
402                description: "doesn't give a damn",
403            },
404
405            (Theme::Weather, TrustMode::Calm) => ModeDisplay {
406                name: "drizzle",
407                emoji: "🌧",
408                color: ModeColor::Cyan,
409                description: "light rain, step carefully",
410            },
411            (Theme::Weather, TrustMode::Steady) => ModeDisplay {
412                name: "clear",
413                emoji: "☀️",
414                color: ModeColor::Green,
415                description: "clear sky, normal pace",
416            },
417            (Theme::Weather, TrustMode::Eager) => ModeDisplay {
418                name: "storm",
419                emoji: "⛈",
420                color: ModeColor::Yellow,
421                description: "storm, coat on, push forward",
422            },
423            (Theme::Weather, TrustMode::Reckless) => ModeDisplay {
424                name: "tornado",
425                emoji: "🌪",
426                color: ModeColor::Red,
427                description: "tornado, hold nothing back",
428            },
429
430            (Theme::Drink, TrustMode::Calm) => ModeDisplay {
431                name: "water",
432                emoji: "💧",
433                color: ModeColor::Cyan,
434                description: "plain and safe",
435            },
436            (Theme::Drink, TrustMode::Steady) => ModeDisplay {
437                name: "coffee",
438                emoji: "☕",
439                color: ModeColor::Green,
440                description: "normal kick",
441            },
442            (Theme::Drink, TrustMode::Eager) => ModeDisplay {
443                name: "espresso",
444                emoji: "☕",
445                color: ModeColor::Yellow,
446                description: "double shot, go fast",
447            },
448            (Theme::Drink, TrustMode::Reckless) => ModeDisplay {
449                name: "bleach",
450                emoji: "🧪",
451                color: ModeColor::Red,
452                description: "drink it and it's gone",
453            },
454        }
455    }
456
457    pub fn escalation_display(&self, escalation: EscalationPolicy) -> EscalationDisplay {
458        match (self, escalation) {
459            (Theme::Default, EscalationPolicy::Deny) => EscalationDisplay {
460                name: "deny",
461                emoji: "🔒",
462                color: ModeColor::Orange,
463            },
464            (Theme::Default, EscalationPolicy::Ask) => EscalationDisplay {
465                name: "ask",
466                emoji: "⚠️",
467                color: ModeColor::Yellow,
468            },
469            (Theme::Default, EscalationPolicy::Allow) => EscalationDisplay {
470                name: "allow",
471                emoji: "✅",
472                color: ModeColor::Green,
473            },
474            (Theme::Wuxia, EscalationPolicy::Deny) => EscalationDisplay {
475                name: "画地为牢",
476                emoji: "⛩️",
477                color: ModeColor::Orange,
478            },
479            (Theme::Wuxia, EscalationPolicy::Ask) => EscalationDisplay {
480                name: "请示",
481                emoji: "📜",
482                color: ModeColor::Yellow,
483            },
484            (Theme::Wuxia, EscalationPolicy::Allow) => EscalationDisplay {
485                name: "放行",
486                emoji: "🎋",
487                color: ModeColor::Green,
488            },
489            (Theme::Animal, EscalationPolicy::Deny) => EscalationDisplay {
490                name: "turtle",
491                emoji: "🐢",
492                color: ModeColor::Orange,
493            },
494            (Theme::Animal, EscalationPolicy::Ask) => EscalationDisplay {
495                name: "owl",
496                emoji: "🦉",
497                color: ModeColor::Yellow,
498            },
499            (Theme::Animal, EscalationPolicy::Allow) => EscalationDisplay {
500                name: "bird",
501                emoji: "🐦",
502                color: ModeColor::Green,
503            },
504            (Theme::Weather, EscalationPolicy::Deny) => EscalationDisplay {
505                name: "fog",
506                emoji: "🌫",
507                color: ModeColor::Orange,
508            },
509            (Theme::Weather, EscalationPolicy::Ask) => EscalationDisplay {
510                name: "cloud",
511                emoji: "☁️",
512                color: ModeColor::Yellow,
513            },
514            (Theme::Weather, EscalationPolicy::Allow) => EscalationDisplay {
515                name: "clear",
516                emoji: "☀️",
517                color: ModeColor::Green,
518            },
519            (Theme::Drink, EscalationPolicy::Deny) => EscalationDisplay {
520                name: "lock-in",
521                emoji: "🍺",
522                color: ModeColor::Orange,
523            },
524            (Theme::Drink, EscalationPolicy::Ask) => EscalationDisplay {
525                name: "card",
526                emoji: "💳",
527                color: ModeColor::Yellow,
528            },
529            (Theme::Drink, EscalationPolicy::Allow) => EscalationDisplay {
530                name: "open-tab",
531                emoji: "🧾",
532                color: ModeColor::Green,
533            },
534        }
535    }
536}
537
538/// User-configurable trust settings.
539#[derive(
540    Debug,
541    Clone,
542    Default,
543    PartialEq,
544    Eq,
545    serde::Serialize,
546    serde::Deserialize,
547    documented::Documented,
548    documented::DocumentedFields,
549)]
550#[serde(deny_unknown_fields)]
551pub struct TrustConfig {
552    /// How aggressively tools are auto-approved.
553    #[serde(default)]
554    pub mode: TrustMode,
555    /// Display theme for trust mode labels.
556    #[serde(default)]
557    pub theme: Theme,
558    /// How Eager mode handles policy decisions that require escalation.
559    #[serde(default)]
560    pub escalation: EscalationPolicy,
561    /// Mode-specific Tier policy overrides.
562    #[serde(default)]
563    pub tiers: TierPolicyConfig,
564    /// Mode-specific resource-risk policy overrides.
565    #[serde(default)]
566    pub risks: RiskPolicyConfig,
567}
568
569impl TrustConfig {
570    pub fn display(&self) -> ModeDisplay {
571        self.theme.display(self.mode)
572    }
573
574    pub fn execution_policy(&self) -> ExecutionPolicy {
575        match self.mode {
576            TrustMode::Reckless => ExecutionPolicy::Unrestricted,
577            TrustMode::Calm | TrustMode::Steady | TrustMode::Eager => ExecutionPolicy::Controlled,
578        }
579    }
580
581    pub fn resolve_tier(&self, tier: crate::tool::Tier) -> PolicyAction {
582        match self.mode {
583            TrustMode::Calm => match tier {
584                crate::tool::Tier::Zero => PolicyAction::Auto,
585                crate::tool::Tier::One
586                | crate::tool::Tier::Two
587                | crate::tool::Tier::Three
588                | crate::tool::Tier::Four => PolicyAction::Ask,
589            },
590            TrustMode::Steady => match tier {
591                crate::tool::Tier::Zero | crate::tool::Tier::One => PolicyAction::Auto,
592                crate::tool::Tier::Two | crate::tool::Tier::Three | crate::tool::Tier::Four => {
593                    PolicyAction::Ask
594                }
595            },
596            TrustMode::Eager => self.tiers.eager.resolve(tier),
597            TrustMode::Reckless => PolicyAction::Auto,
598        }
599    }
600
601    pub fn resolve_risk(&self, risk: RiskKind) -> PolicyAction {
602        match self.mode {
603            TrustMode::Calm | TrustMode::Steady => PolicyAction::Ask,
604            TrustMode::Eager => self.risks.eager.resolve(risk),
605            TrustMode::Reckless => PolicyAction::Auto,
606        }
607    }
608
609    /// Resolves the controlled policy before grants or hard-boundary checks.
610    pub fn resolve_policy(
611        &self,
612        tier: crate::tool::Tier,
613        risks: impl IntoIterator<Item = RiskKind>,
614    ) -> PolicyAction {
615        self.resolve_policy_resolution(tier, risks).action
616    }
617
618    pub fn resolve_policy_resolution(
619        &self,
620        tier: crate::tool::Tier,
621        risks: impl IntoIterator<Item = RiskKind>,
622    ) -> PolicyResolution {
623        if self.execution_policy() == ExecutionPolicy::Unrestricted {
624            return PolicyResolution {
625                action: PolicyAction::Auto,
626                escalation: PolicyEscalation::None,
627            };
628        }
629
630        let action = risks
631            .into_iter()
632            .fold(self.resolve_tier(tier), |action, risk| {
633                action.most_restrictive(self.resolve_risk(risk))
634            });
635
636        if self.mode != TrustMode::Eager || action != PolicyAction::Ask {
637            return PolicyResolution {
638                action,
639                escalation: PolicyEscalation::None,
640            };
641        }
642        match self.escalation {
643            EscalationPolicy::Deny => PolicyResolution {
644                action: PolicyAction::Deny,
645                escalation: PolicyEscalation::Denied,
646            },
647            EscalationPolicy::Ask => PolicyResolution {
648                action: PolicyAction::Ask,
649                escalation: PolicyEscalation::Pending,
650            },
651            EscalationPolicy::Allow => PolicyResolution {
652                action: PolicyAction::Auto,
653                escalation: PolicyEscalation::Allowed,
654            },
655        }
656    }
657}
658
659#[cfg(test)]
660mod tests {
661    use super::*;
662
663    #[test]
664    fn sandbox_disabled_only_for_reckless() {
665        assert!(TrustMode::Calm.sandbox_enabled());
666        assert!(TrustMode::Steady.sandbox_enabled());
667        assert!(TrustMode::Eager.sandbox_enabled());
668        assert!(!TrustMode::Reckless.sandbox_enabled());
669    }
670
671    #[test]
672    fn needs_warning_for_eager_and_reckless() {
673        assert!(!TrustMode::Calm.needs_warning());
674        assert!(!TrustMode::Steady.needs_warning());
675        assert!(TrustMode::Eager.needs_warning());
676        assert!(TrustMode::Reckless.needs_warning());
677    }
678
679    #[test]
680    fn warning_text_includes_mode_name() {
681        let cfg = TrustConfig {
682            mode: TrustMode::Eager,
683            theme: Theme::Default,
684            ..TrustConfig::default()
685        };
686        let display = cfg.display();
687        let warning = TrustMode::Eager.warning(&display).unwrap();
688        assert!(warning.contains("eager"));
689
690        let cfg2 = TrustConfig {
691            mode: TrustMode::Reckless,
692            theme: Theme::Animal,
693            ..TrustConfig::default()
694        };
695        let display2 = cfg2.display();
696        let warning2 = TrustMode::Reckless.warning(&display2).unwrap();
697        assert!(warning2.contains("honey-badger"));
698    }
699
700    #[test]
701    fn wuxia_escalation_display_preserves_localized_labels() {
702        assert_eq!(
703            Theme::Wuxia.escalation_display(EscalationPolicy::Deny).name,
704            "画地为牢"
705        );
706        assert_eq!(
707            Theme::Wuxia.escalation_display(EscalationPolicy::Ask).name,
708            "请示"
709        );
710        assert_eq!(
711            Theme::Wuxia
712                .escalation_display(EscalationPolicy::Allow)
713                .name,
714            "放行"
715        );
716    }
717
718    #[test]
719    fn all_themes_produce_displays() {
720        let themes = [
721            Theme::Default,
722            Theme::Wuxia,
723            Theme::Animal,
724            Theme::Weather,
725            Theme::Drink,
726        ];
727        for theme in &themes {
728            for mode in &TrustMode::all() {
729                let d = theme.display(*mode);
730                assert!(!d.name.is_empty());
731                assert!(!d.emoji.is_empty());
732                assert!(!d.description.is_empty());
733            }
734        }
735    }
736
737    #[test]
738    fn mode_from_str_parses_all_variants() {
739        assert_eq!("calm".parse::<TrustMode>().unwrap(), TrustMode::Calm);
740        assert_eq!("steady".parse::<TrustMode>().unwrap(), TrustMode::Steady);
741        assert_eq!("eager".parse::<TrustMode>().unwrap(), TrustMode::Eager);
742        assert_eq!(
743            "reckless".parse::<TrustMode>().unwrap(),
744            TrustMode::Reckless
745        );
746        assert_eq!("yolo".parse::<TrustMode>().unwrap(), TrustMode::Reckless);
747        assert_eq!("1".parse::<TrustMode>().unwrap(), TrustMode::Calm);
748        assert_eq!("4".parse::<TrustMode>().unwrap(), TrustMode::Reckless);
749        assert!("unknown".parse::<TrustMode>().is_err());
750    }
751
752    #[test]
753    fn theme_from_str_parses_all_variants() {
754        assert_eq!("default".parse::<Theme>().unwrap(), Theme::Default);
755        assert_eq!("wuxia".parse::<Theme>().unwrap(), Theme::Wuxia);
756        assert_eq!("animal".parse::<Theme>().unwrap(), Theme::Animal);
757        assert_eq!("weather".parse::<Theme>().unwrap(), Theme::Weather);
758        assert_eq!("drink".parse::<Theme>().unwrap(), Theme::Drink);
759        assert!("unknown".parse::<Theme>().is_err());
760    }
761
762    #[test]
763    fn theme_display_roundtrip() {
764        for theme in &[
765            Theme::Default,
766            Theme::Wuxia,
767            Theme::Animal,
768            Theme::Weather,
769            Theme::Drink,
770        ] {
771            let s = theme.to_string();
772            let back: Theme = s.parse().unwrap();
773            assert_eq!(*theme, back);
774        }
775    }
776
777    #[test]
778    fn default_config_is_steady_approve() {
779        let cfg = TrustConfig::default();
780        assert_eq!(cfg.mode, TrustMode::Steady);
781        assert_eq!(cfg.theme, Theme::Default);
782        assert_eq!(cfg.escalation, EscalationPolicy::Ask);
783    }
784
785    #[test]
786    fn wuxia_descriptions_are_chinese() {
787        let d = Theme::Wuxia.display(TrustMode::Calm);
788        assert!(d.description.contains("拙"));
789        let d = Theme::Wuxia.display(TrustMode::Steady);
790        assert!(d.description.contains("行云"));
791    }
792
793    #[test]
794    fn non_wuxia_descriptions_are_english() {
795        for mode in &TrustMode::all() {
796            let d = Theme::Default.display(*mode);
797            assert!(
798                d.description.is_ascii(),
799                "default theme should be ASCII: {}",
800                d.description
801            );
802            let d = Theme::Animal.display(*mode);
803            assert!(
804                d.description.is_ascii(),
805                "animal theme should be ASCII: {}",
806                d.description
807            );
808        }
809    }
810
811    #[test]
812    fn four_levels_ordered() {
813        assert_eq!(TrustMode::Calm.level(), 1);
814        assert_eq!(TrustMode::Steady.level(), 2);
815        assert_eq!(TrustMode::Eager.level(), 3);
816        assert_eq!(TrustMode::Reckless.level(), 4);
817    }
818
819    #[test]
820    fn default_tier_matrix_matches_modes() {
821        use crate::tool::Tier;
822
823        let tiers = [Tier::Zero, Tier::One, Tier::Two, Tier::Three, Tier::Four];
824        let cases = [
825            (
826                TrustMode::Calm,
827                [
828                    PolicyAction::Auto,
829                    PolicyAction::Ask,
830                    PolicyAction::Ask,
831                    PolicyAction::Ask,
832                    PolicyAction::Ask,
833                ],
834            ),
835            (
836                TrustMode::Steady,
837                [
838                    PolicyAction::Auto,
839                    PolicyAction::Auto,
840                    PolicyAction::Ask,
841                    PolicyAction::Ask,
842                    PolicyAction::Ask,
843                ],
844            ),
845            (
846                TrustMode::Eager,
847                [
848                    PolicyAction::Auto,
849                    PolicyAction::Auto,
850                    PolicyAction::Ask,
851                    PolicyAction::Ask,
852                    PolicyAction::Ask,
853                ],
854            ),
855            (TrustMode::Reckless, [PolicyAction::Auto; 5]),
856        ];
857
858        for (mode, expected) in cases {
859            let config = TrustConfig {
860                mode,
861                ..TrustConfig::default()
862            };
863            for (tier, action) in tiers.into_iter().zip(expected) {
864                assert_eq!(
865                    config.resolve_tier(tier),
866                    action,
867                    "mode={mode:?}, tier={tier:?}"
868                );
869            }
870        }
871    }
872
873    #[test]
874    fn calm_and_steady_keep_fixed_safety_floors() {
875        use crate::tool::Tier;
876
877        let configured = TierPolicyConfig {
878            eager: TierPolicyOverrides {
879                tier4: Some(PolicyAction::Auto),
880                ..TierPolicyOverrides::default()
881            },
882        };
883        for mode in [TrustMode::Calm, TrustMode::Steady] {
884            let config = TrustConfig {
885                mode,
886                tiers: configured.clone(),
887                ..TrustConfig::default()
888            };
889            assert_eq!(config.resolve_tier(Tier::Four), PolicyAction::Ask);
890        }
891    }
892
893    #[test]
894    fn eager_allow_never_overrides_explicit_deny() {
895        use crate::tool::Tier;
896
897        let config = TrustConfig {
898            mode: TrustMode::Eager,
899            escalation: EscalationPolicy::Allow,
900            tiers: TierPolicyConfig {
901                eager: TierPolicyOverrides {
902                    tier4: Some(PolicyAction::Deny),
903                    ..TierPolicyOverrides::default()
904                },
905            },
906            risks: RiskPolicyConfig {
907                eager: RiskPolicyOverrides {
908                    network: Some(PolicyAction::Deny),
909                    ..RiskPolicyOverrides::default()
910                },
911            },
912            ..TrustConfig::default()
913        };
914
915        assert_eq!(config.resolve_policy(Tier::Four, []), PolicyAction::Deny);
916        assert_eq!(
917            config.resolve_policy(Tier::Zero, [RiskKind::Network]),
918            PolicyAction::Deny
919        );
920        assert_eq!(config.resolve_policy(Tier::Three, []), PolicyAction::Auto);
921    }
922
923    #[test]
924    fn eager_escalation_applies_only_to_ask() {
925        use crate::tool::Tier;
926
927        for (escalation, expected) in [
928            (EscalationPolicy::Deny, PolicyAction::Deny),
929            (EscalationPolicy::Ask, PolicyAction::Ask),
930            (EscalationPolicy::Allow, PolicyAction::Auto),
931        ] {
932            let config = TrustConfig {
933                mode: TrustMode::Eager,
934                escalation,
935                ..TrustConfig::default()
936            };
937            assert_eq!(config.resolve_policy(Tier::Three, []), expected);
938            assert_eq!(config.resolve_policy(Tier::Zero, []), PolicyAction::Auto);
939        }
940    }
941
942    #[test]
943    fn every_risk_defaults_to_ask_in_controlled_modes() {
944        let risks = [
945            RiskKind::WorkspaceExternal,
946            RiskKind::Network,
947            RiskKind::Irreversible,
948            RiskKind::FilesystemWrite,
949            RiskKind::ProcessSpawn,
950            RiskKind::RepositoryMutation,
951        ];
952        for mode in [TrustMode::Calm, TrustMode::Steady, TrustMode::Eager] {
953            let config = TrustConfig {
954                mode,
955                ..TrustConfig::default()
956            };
957            for risk in risks {
958                assert_eq!(config.resolve_risk(risk), PolicyAction::Ask);
959            }
960        }
961    }
962
963    #[test]
964    fn reckless_is_explicitly_unrestricted() {
965        use crate::tool::Tier;
966
967        let config = TrustConfig {
968            mode: TrustMode::Reckless,
969            escalation: EscalationPolicy::Deny,
970            ..TrustConfig::default()
971        };
972        assert_eq!(config.execution_policy(), ExecutionPolicy::Unrestricted);
973        assert_eq!(
974            config.resolve_policy(Tier::Four, [RiskKind::Network]),
975            PolicyAction::Auto
976        );
977    }
978
979    #[test]
980    fn eager_resolution_preserves_how_ask_was_resolved() {
981        use crate::tool::Tier;
982
983        let allowed = TrustConfig {
984            mode: TrustMode::Eager,
985            escalation: EscalationPolicy::Allow,
986            ..TrustConfig::default()
987        }
988        .resolve_policy_resolution(Tier::Two, [RiskKind::ProcessSpawn]);
989        assert_eq!(allowed.action, PolicyAction::Auto);
990        assert_eq!(allowed.escalation, PolicyEscalation::Allowed);
991
992        let automatic = TrustConfig {
993            mode: TrustMode::Eager,
994            tiers: TierPolicyConfig {
995                eager: TierPolicyOverrides {
996                    tier2: Some(PolicyAction::Auto),
997                    ..TierPolicyOverrides::default()
998                },
999            },
1000            risks: RiskPolicyConfig {
1001                eager: RiskPolicyOverrides {
1002                    process_spawn: Some(PolicyAction::Auto),
1003                    ..RiskPolicyOverrides::default()
1004                },
1005            },
1006            ..TrustConfig::default()
1007        }
1008        .resolve_policy_resolution(Tier::Two, [RiskKind::ProcessSpawn]);
1009        assert_eq!(automatic.action, PolicyAction::Auto);
1010        assert_eq!(automatic.escalation, PolicyEscalation::None);
1011    }
1012
1013    #[test]
1014    fn trust_config_json_and_toml_round_trip() {
1015        let config = TrustConfig {
1016            mode: TrustMode::Eager,
1017            escalation: EscalationPolicy::Allow,
1018            tiers: TierPolicyConfig {
1019                eager: TierPolicyOverrides {
1020                    tier4: Some(PolicyAction::Deny),
1021                    ..TierPolicyOverrides::default()
1022                },
1023            },
1024            risks: RiskPolicyConfig {
1025                eager: RiskPolicyOverrides {
1026                    network: Some(PolicyAction::Ask),
1027                    ..RiskPolicyOverrides::default()
1028                },
1029            },
1030            ..TrustConfig::default()
1031        };
1032
1033        let json = serde_json::to_string(&config).unwrap();
1034        let from_json: TrustConfig = serde_json::from_str(&json).unwrap();
1035        let toml = toml::to_string(&config).unwrap();
1036        let from_toml: TrustConfig = toml::from_str(&toml).unwrap();
1037        for back in [from_json, from_toml] {
1038            assert_eq!(back.mode, config.mode);
1039            assert_eq!(back.escalation, config.escalation);
1040            assert_eq!(back.tiers, config.tiers);
1041            assert_eq!(back.risks, config.risks);
1042        }
1043    }
1044}