Skip to main content

a3s_code_core/permissions/
risk.rs

1//! Structured risk vocabulary for interactive tool guardrails.
2//!
3//! Risk is deliberately not represented by a single opaque score. Each
4//! assessment records the tool type, operation target, blast radius,
5//! reversibility, and environment sensitivity, then assigns one of four levels.
6//! Interactive hosts use the level-to-action matrix in `InteractiveApprovalMode`.
7
8use serde::{Deserialize, Serialize};
9
10/// Coarse risk level for one tool invocation.
11///
12/// Levels are ordered from least to most restrictive so composite tools can
13/// inherit the highest-risk child.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum ToolRiskLevel {
17    /// Read-only, bounded, and understood well enough to run without friction.
18    Routine,
19    /// A reversible mutation confined to the current workspace.
20    Bounded,
21    /// Context-dependent, unbounded, external, or otherwise ambiguous execution.
22    High,
23    /// A deterministic safety-boundary violation that no mode or model may bypass.
24    Critical,
25}
26
27/// Routing action selected from a risk assessment and interactive mode.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum ToolRiskAction {
31    /// Execute without a confirmation prompt.
32    Allow,
33    /// A deterministic policy requires explicit human approval.
34    RequireConfirmation,
35    /// Ask a constrained LLM reviewer whether to allow, escalate, or deny.
36    ReviewByLlm,
37    /// Reject by deterministic rule; an LLM review cannot override this action.
38    RuleDeny,
39}
40
41/// Primary capability exercised by a tool invocation.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum ToolRiskType {
45    ReadOnly,
46    WorkspaceMutation,
47    CommandExecution,
48    VersionControl,
49    NetworkRead,
50    Delegation,
51    DynamicExecution,
52    ExternalIntegration,
53    Composite,
54    Unknown,
55}
56
57/// Object or boundary targeted by an invocation.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum OperationTarget {
61    Workspace,
62    OutsideWorkspace,
63    PublicNetwork,
64    ExternalService,
65    HostEnvironment,
66    PrivilegedSystem,
67    Multiple,
68    Unknown,
69}
70
71/// Maximum expected blast radius if the invocation succeeds.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum ImpactScope {
75    Observation,
76    SingleResource,
77    Workspace,
78    ExternalService,
79    Host,
80    SystemWide,
81    Unknown,
82}
83
84/// How readily the direct effects can be undone.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub enum Reversibility {
88    NotApplicable,
89    Easy,
90    Conditional,
91    Difficult,
92    Irreversible,
93    Unknown,
94}
95
96/// Ambient execution environment that can affect the operation's safety.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub enum EnvironmentSensitivity {
100    None,
101    Workspace,
102    Network,
103    Host,
104    Credentials,
105    Privileged,
106    Mixed,
107    Unknown,
108}
109
110/// Stable, non-secret reason codes explaining a classification.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum ToolRiskReason {
114    KnownReadOnly,
115    BoundedWorkspaceMutation,
116    BoundedVersionControlMutation,
117    UnboundedCommandExecution,
118    DynamicOrDelegatedExecution,
119    ExternalOrUnknownIntegration,
120    EnvironmentSensitiveOperation,
121    ForceOrDestructiveOperation,
122    MalformedOrUnknownOperation,
123    WorkspaceBoundaryEscape,
124    SymlinkBoundaryEscape,
125    CatastrophicOperation,
126    CompositeInvocation,
127}
128
129/// The five independent dimensions used to explain a risk level.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
131pub struct ToolRiskDimensions {
132    pub tool_type: ToolRiskType,
133    pub operation_target: OperationTarget,
134    pub impact_scope: ImpactScope,
135    pub reversibility: Reversibility,
136    pub environment_sensitivity: EnvironmentSensitivity,
137}
138
139impl ToolRiskDimensions {
140    pub const fn new(
141        tool_type: ToolRiskType,
142        operation_target: OperationTarget,
143        impact_scope: ImpactScope,
144        reversibility: Reversibility,
145        environment_sensitivity: EnvironmentSensitivity,
146    ) -> Self {
147        Self {
148            tool_type,
149            operation_target,
150            impact_scope,
151            reversibility,
152            environment_sensitivity,
153        }
154    }
155}
156
157/// Explainable risk assessment produced before tool execution.
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159pub struct ToolRiskAssessment {
160    pub level: ToolRiskLevel,
161    pub dimensions: ToolRiskDimensions,
162    pub reasons: Vec<ToolRiskReason>,
163}
164
165impl ToolRiskAssessment {
166    pub fn new(
167        level: ToolRiskLevel,
168        dimensions: ToolRiskDimensions,
169        reasons: impl IntoIterator<Item = ToolRiskReason>,
170    ) -> Self {
171        let mut deduplicated = Vec::new();
172        for reason in reasons {
173            if !deduplicated.contains(&reason) {
174                deduplicated.push(reason);
175            }
176        }
177        Self {
178            level,
179            dimensions,
180            reasons: deduplicated,
181        }
182    }
183
184    /// Conservative action before a host applies mode-specific streamlining.
185    ///
186    /// Auto mode may promote only `Bounded` to `Allow`. `High` always remains
187    /// an LLM-review candidate and `Critical` always remains a rule denial.
188    pub const fn baseline_action(&self) -> ToolRiskAction {
189        match self.level {
190            ToolRiskLevel::Routine => ToolRiskAction::Allow,
191            ToolRiskLevel::Bounded => ToolRiskAction::RequireConfirmation,
192            ToolRiskLevel::High => ToolRiskAction::ReviewByLlm,
193            ToolRiskLevel::Critical => ToolRiskAction::RuleDeny,
194        }
195    }
196}