use std::path::Path;
use crate::sandbox::SandboxPolicy;
use crate::tools::spec::ApprovalRequirement;
use crate::tui::app::AppMode;
use crate::tui::approval::ApprovalMode;
use crate::worker_profile::ShellPolicy;
use super::ops::UserInputProvenance;
#[derive(Debug, Clone, Copy)]
pub(crate) struct ModeSessionPrefs {
pub(crate) agent_allow_shell: bool,
pub(crate) agent_trust_mode: bool,
pub(crate) agent_approval_mode: ApprovalMode,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct EffectiveModePolicy {
#[allow(dead_code)]
pub(crate) mode: AppMode,
pub(crate) allow_shell: bool,
pub(crate) trust_mode: bool,
pub(crate) approval_mode: ApprovalMode,
}
#[must_use]
pub(crate) fn base_policy_for_mode(mode: AppMode, prefs: &ModeSessionPrefs) -> EffectiveModePolicy {
match mode {
AppMode::Plan => EffectiveModePolicy {
mode,
allow_shell: false,
trust_mode: false,
approval_mode: ApprovalMode::Suggest,
},
AppMode::Agent | AppMode::Auto | AppMode::Operate => EffectiveModePolicy {
mode,
allow_shell: prefs.agent_allow_shell,
trust_mode: prefs.agent_trust_mode,
approval_mode: prefs.agent_approval_mode,
},
AppMode::Yolo => EffectiveModePolicy {
mode,
allow_shell: true,
trust_mode: true,
approval_mode: ApprovalMode::Bypass,
},
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PolicyNarrowingReason {
NonAuthoritativeProvenance,
}
impl PolicyNarrowingReason {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::NonAuthoritativeProvenance => "non_authoritative_provenance",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PolicyNarrowingEvent {
reason: PolicyNarrowingReason,
from_mode: &'static str,
to_mode: &'static str,
from_approval: ApprovalMode,
to_approval: ApprovalMode,
detail: String,
}
impl PolicyNarrowingEvent {
pub(crate) fn reason(&self) -> PolicyNarrowingReason {
self.reason
}
pub(crate) fn message(&self) -> String {
match self.reason {
PolicyNarrowingReason::NonAuthoritativeProvenance => format!(
"Input provenance '{}' cannot inherit standing auto-approval authority; continuing with approvals required.",
self.detail
),
}
}
pub(crate) fn transition(&self) -> String {
format!(
"{} ({}) -> {} ({})",
self.from_mode,
self.from_approval.permission_chip_label(),
self.to_mode,
self.to_approval.permission_chip_label(),
)
}
}
#[derive(Debug, Clone)]
pub(crate) struct TurnAuthority {
pub(crate) mode: AppMode,
pub(crate) allow_shell: bool,
pub(crate) trust_mode: bool,
pub(crate) auto_approve: bool,
pub(crate) approval_mode: ApprovalMode,
pub(crate) dynamic_active_tools: Vec<&'static str>,
pub(crate) narrowing: Option<PolicyNarrowingEvent>,
}
impl TurnAuthority {
pub(crate) fn status(&self) -> Option<String> {
self.narrowing.as_ref().map(PolicyNarrowingEvent::message)
}
#[must_use]
pub(crate) fn from_effective_fields(
mode: AppMode,
allow_shell: bool,
trust_mode: bool,
auto_approve: bool,
approval_mode: ApprovalMode,
) -> Self {
Self {
mode,
allow_shell,
trust_mode,
auto_approve,
approval_mode,
dynamic_active_tools: Vec::new(),
narrowing: None,
}
}
#[must_use]
pub(crate) fn approval_mode_for_session(&self) -> ApprovalMode {
agent_approval_mode_for_turn(self.auto_approve, self.approval_mode)
}
#[must_use]
pub(crate) fn for_tool_approval_decision(auto_approve: bool) -> Self {
Self::from_effective_fields(
if auto_approve {
AppMode::Yolo
} else {
AppMode::Agent
},
true,
false,
auto_approve,
if auto_approve {
ApprovalMode::Bypass
} else {
ApprovalMode::Suggest
},
)
}
#[must_use]
pub(crate) fn shell_policy(&self) -> ShellPolicy {
shell_policy_for_mode(self.mode, self.allow_shell)
}
#[must_use]
pub(crate) fn sandbox_policy(
&self,
workspace: &Path,
configured_mode: Option<&str>,
) -> SandboxPolicy {
sandbox_policy_for_turn(
self.mode,
self.approval_mode_for_session(),
configured_mode,
workspace,
)
}
}
#[must_use]
pub(crate) fn effective_input_policy(
provenance: UserInputProvenance,
requested_mode: AppMode,
_content: &str,
allow_shell: bool,
trust_mode: bool,
auto_approve: bool,
approval_mode: ApprovalMode,
) -> TurnAuthority {
let mut mode = requested_mode;
let mut trust_mode = trust_mode;
let mut auto_approve = auto_approve;
let mut approval_mode = approval_mode;
let mut narrowing = None;
if !provenance_can_inherit_standing_auto_authority(provenance) {
let from_mode = mode;
let from_approval = approval_mode;
let had_auto_authority = matches!(mode, AppMode::Yolo)
|| trust_mode
|| auto_approve
|| matches!(approval_mode, ApprovalMode::Bypass);
if matches!(mode, AppMode::Yolo) {
mode = AppMode::Agent;
}
trust_mode = false;
auto_approve = false;
if matches!(approval_mode, ApprovalMode::Auto | ApprovalMode::Bypass) {
approval_mode = ApprovalMode::Suggest;
}
if had_auto_authority {
narrowing = Some(PolicyNarrowingEvent {
reason: PolicyNarrowingReason::NonAuthoritativeProvenance,
from_mode: from_mode.as_setting(),
to_mode: mode.as_setting(),
from_approval,
to_approval: approval_mode,
detail: provenance.as_str().to_string(),
});
}
}
if approval_mode == ApprovalMode::Bypass {
auto_approve = true;
}
TurnAuthority {
mode,
allow_shell,
trust_mode,
auto_approve,
approval_mode,
dynamic_active_tools: Vec::new(),
narrowing,
}
}
#[must_use]
pub(crate) fn provenance_can_inherit_standing_auto_authority(
provenance: UserInputProvenance,
) -> bool {
matches!(
provenance,
UserInputProvenance::ExternalUser
| UserInputProvenance::Runtime
| UserInputProvenance::SubAgentHandoff
)
}
#[must_use]
pub(crate) fn permission_posture_allows_questions(approval_mode: ApprovalMode) -> bool {
approval_mode != ApprovalMode::Auto
}
#[must_use]
pub(crate) fn agent_approval_mode_for_turn(
auto_approve: bool,
approval_mode: ApprovalMode,
) -> ApprovalMode {
if auto_approve {
ApprovalMode::Bypass
} else {
approval_mode
}
}
#[must_use]
pub(crate) fn sandbox_policy_for_turn(
mode: AppMode,
approval_mode: ApprovalMode,
configured_mode: Option<&str>,
workspace: &Path,
) -> SandboxPolicy {
let default = if mode == AppMode::Plan {
SandboxPolicy::ReadOnly
} else if mode == AppMode::Yolo || approval_mode == ApprovalMode::Bypass {
SandboxPolicy::DangerFullAccess
} else {
workspace_write_policy(workspace)
};
match (default, configured_mode) {
(SandboxPolicy::ReadOnly, _) | (_, Some("read-only")) => SandboxPolicy::ReadOnly,
(SandboxPolicy::DangerFullAccess, Some("workspace-write")) => {
workspace_write_policy(workspace)
}
(SandboxPolicy::DangerFullAccess, Some("external-sandbox")) => {
SandboxPolicy::ExternalSandbox {
network_access: true,
}
}
(policy, _) => policy,
}
}
fn workspace_write_policy(workspace: &Path) -> SandboxPolicy {
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![workspace.to_path_buf()],
network_access: true,
exclude_tmpdir: false,
exclude_slash_tmp: false,
}
}
#[must_use]
pub(crate) fn shell_policy_for_mode(mode: AppMode, allow_shell: bool) -> ShellPolicy {
if !allow_shell {
return ShellPolicy::None;
}
match mode {
AppMode::Plan => ShellPolicy::None,
AppMode::Agent | AppMode::Auto | AppMode::Operate | AppMode::Yolo => ShellPolicy::Full,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ToolPermission {
Allow,
Prompt,
Deny,
}
#[must_use]
pub(crate) fn resolve_tool_permission(
authority: &TurnAuthority,
requirement: ApprovalRequirement,
is_non_bypassable: bool,
) -> ToolPermission {
if authority.approval_mode == ApprovalMode::Never
&& requirement != ApprovalRequirement::Auto
&& !authority.auto_approve
&& authority.mode != AppMode::Yolo
{
return ToolPermission::Deny;
}
match requirement {
ApprovalRequirement::Auto => ToolPermission::Allow,
ApprovalRequirement::Suggest | ApprovalRequirement::Required => {
if is_non_bypassable {
return ToolPermission::Prompt;
}
if authority.auto_approve
|| authority.approval_mode == ApprovalMode::Bypass
|| authority.mode == AppMode::Yolo
{
ToolPermission::Allow
} else {
ToolPermission::Prompt
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ApprovalRequestDisposition {
AutoApprove,
AutoDenySessionDenied,
AutoDenyFullAccessPolicyHold,
AutoDenyNeverPosture,
Prompt,
}
#[must_use]
pub(crate) fn resolve_approval_request_disposition(
authority: &TurnAuthority,
session_approved: bool,
session_denied: bool,
approval_force_prompt: bool,
) -> ApprovalRequestDisposition {
if session_denied {
return ApprovalRequestDisposition::AutoDenySessionDenied;
}
let posture = resolve_tool_permission(authority, ApprovalRequirement::Suggest, false);
if approval_force_prompt && posture == ToolPermission::Allow {
return ApprovalRequestDisposition::AutoDenyFullAccessPolicyHold;
}
if !approval_force_prompt && (posture == ToolPermission::Allow || session_approved) {
return ApprovalRequestDisposition::AutoApprove;
}
if posture == ToolPermission::Deny {
return ApprovalRequestDisposition::AutoDenyNeverPosture;
}
ApprovalRequestDisposition::Prompt
}
#[cfg(test)]
mod tests {
use super::*;
fn authority(mode: AppMode, auto_approve: bool, approval_mode: ApprovalMode) -> TurnAuthority {
TurnAuthority::from_effective_fields(mode, true, false, auto_approve, approval_mode)
}
#[test]
fn full_access_is_unsandboxed_unless_effective_config_is_stricter() {
let workspace = Path::new("/work");
let full_access = authority(AppMode::Agent, true, ApprovalMode::Bypass);
assert_eq!(
full_access.sandbox_policy(workspace, None),
SandboxPolicy::DangerFullAccess
);
assert!(matches!(
full_access.sandbox_policy(workspace, Some("workspace-write")),
SandboxPolicy::WorkspaceWrite { writable_roots, .. }
if writable_roots == vec![workspace.to_path_buf()]
));
assert_eq!(
full_access.sandbox_policy(workspace, Some("read-only")),
SandboxPolicy::ReadOnly
);
assert!(matches!(
full_access.sandbox_policy(workspace, Some("external-sandbox")),
SandboxPolicy::ExternalSandbox {
network_access: true
}
));
}
#[test]
fn plan_ask_and_auto_review_cannot_be_loosened_by_sandbox_config() {
let workspace = Path::new("/work");
for approval_mode in [ApprovalMode::Suggest, ApprovalMode::Auto] {
let authority = authority(AppMode::Agent, false, approval_mode);
assert!(matches!(
authority.sandbox_policy(workspace, Some("danger-full-access")),
SandboxPolicy::WorkspaceWrite { .. }
));
}
let plan = authority(AppMode::Plan, true, ApprovalMode::Bypass);
assert_eq!(
plan.sandbox_policy(workspace, Some("danger-full-access")),
SandboxPolicy::ReadOnly
);
}
#[test]
fn auto_requirement_always_allows() {
for (mode, auto_approve, approval_mode) in [
(AppMode::Agent, false, ApprovalMode::Suggest),
(AppMode::Agent, false, ApprovalMode::Auto),
(AppMode::Agent, false, ApprovalMode::Never),
(AppMode::Agent, true, ApprovalMode::Bypass),
(AppMode::Yolo, true, ApprovalMode::Bypass),
(AppMode::Plan, false, ApprovalMode::Suggest),
] {
let auth = authority(mode, auto_approve, approval_mode);
for non_bypassable in [false, true] {
assert_eq!(
resolve_tool_permission(&auth, ApprovalRequirement::Auto, non_bypassable),
ToolPermission::Allow,
"{mode:?}/{auto_approve}/{approval_mode:?}/nb={non_bypassable}"
);
}
}
}
#[test]
fn ask_posture_prompts_for_non_auto_tools() {
let auth = authority(AppMode::Agent, false, ApprovalMode::Suggest);
for requirement in [ApprovalRequirement::Suggest, ApprovalRequirement::Required] {
assert_eq!(
resolve_tool_permission(&auth, requirement, false),
ToolPermission::Prompt
);
assert_eq!(
resolve_tool_permission(&auth, requirement, true),
ToolPermission::Prompt
);
}
}
#[test]
fn full_access_allows_bypassable_but_prompts_for_non_bypassable() {
for auth in [
authority(AppMode::Agent, true, ApprovalMode::Bypass),
authority(AppMode::Yolo, true, ApprovalMode::Bypass),
TurnAuthority::for_tool_approval_decision(true),
] {
for requirement in [ApprovalRequirement::Suggest, ApprovalRequirement::Required] {
assert_eq!(
resolve_tool_permission(&auth, requirement, false),
ToolPermission::Allow,
"generic {requirement:?} tool stays auto-approved in Full Access"
);
assert_eq!(
resolve_tool_permission(&auth, requirement, true),
ToolPermission::Prompt,
"non-bypassable {requirement:?} tool forces a prompt in Full Access"
);
}
}
}
#[test]
fn never_denies_promptable_tools_but_not_reads_or_full_access_shapes() {
let never = authority(AppMode::Agent, false, ApprovalMode::Never);
assert_eq!(
resolve_tool_permission(&never, ApprovalRequirement::Suggest, false),
ToolPermission::Deny
);
assert_eq!(
resolve_tool_permission(&never, ApprovalRequirement::Required, true),
ToolPermission::Deny
);
assert_eq!(
resolve_tool_permission(&never, ApprovalRequirement::Auto, false),
ToolPermission::Allow,
"Never remains read-only rather than dead"
);
let stale = authority(AppMode::Agent, true, ApprovalMode::Never);
assert_eq!(
resolve_tool_permission(&stale, ApprovalRequirement::Suggest, false),
ToolPermission::Allow
);
let yolo_never = authority(AppMode::Yolo, false, ApprovalMode::Never);
assert_eq!(
resolve_tool_permission(&yolo_never, ApprovalRequirement::Suggest, false),
ToolPermission::Allow
);
}
#[test]
fn approval_request_disposition_preserves_legacy_branch_order() {
let ask = authority(AppMode::Agent, false, ApprovalMode::Suggest);
let full_access = authority(AppMode::Agent, true, ApprovalMode::Bypass);
let never = authority(AppMode::Agent, false, ApprovalMode::Never);
assert_eq!(
resolve_approval_request_disposition(&full_access, true, true, false),
ApprovalRequestDisposition::AutoDenySessionDenied
);
assert_eq!(
resolve_approval_request_disposition(&full_access, true, false, true),
ApprovalRequestDisposition::AutoDenyFullAccessPolicyHold
);
assert_eq!(
resolve_approval_request_disposition(&full_access, false, false, false),
ApprovalRequestDisposition::AutoApprove
);
assert_eq!(
resolve_approval_request_disposition(&ask, true, false, false),
ApprovalRequestDisposition::AutoApprove
);
assert_eq!(
resolve_approval_request_disposition(&never, true, false, false),
ApprovalRequestDisposition::AutoApprove
);
assert_eq!(
resolve_approval_request_disposition(&never, false, false, false),
ApprovalRequestDisposition::AutoDenyNeverPosture
);
assert_eq!(
resolve_approval_request_disposition(&ask, false, false, false),
ApprovalRequestDisposition::Prompt
);
}
}