use std::collections::BTreeMap; use std::collections::HashMap; use codex_config::AbsolutePathBuf; use codex_config::AppRequirementToml; use codex_config::AppToolRequirementToml; use codex_config::AppToolsRequirementsToml; use codex_config::AppsRequirementsToml; use codex_config::CONFIG_TOML_FILE; use codex_config::ConfigLayerStack; use codex_config::ConfigRequirements; use codex_config::ConfigRequirementsToml; use codex_config::TomlValue; use codex_config::types::AppConfig; use codex_config::types::AppToolApproval; use codex_config::types::AppToolConfig; use codex_config::types::AppToolsConfig; use codex_config::types::AppsConfigToml; use codex_config::types::AppsDefaultConfig; use pretty_assertions::assert_eq; use super::*; #[test] fn evaluator_reuses_one_snapshot_across_tools() { let apps_config = AppsConfigToml { default: None, apps: HashMap::from([( "calendar".to_string(), AppConfig { enabled: true, default_tools_enabled: Some(false), tools: Some(AppToolsConfig { tools: HashMap::from([( "events/create".to_string(), AppToolConfig { enabled: Some(true), approval_mode: Some(AppToolApproval::Prompt), }, )]), }), ..Default::default() }, )]), }; let requirements = AppsRequirementsToml { apps: BTreeMap::from([( "calendar".to_string(), AppRequirementToml { enabled: None, tools: Some(AppToolsRequirementsToml { tools: BTreeMap::from([( "events/create".to_string(), AppToolRequirementToml { approval_mode: Some(AppToolApproval::Approve), ..Default::default() }, )]), }), }, )]), }; let evaluator = AppToolPolicyEvaluator::from_parts(Some(apps_config), Some(&requirements)); assert_eq!( [ evaluator.policy(input("events/create", /*tool_title*/ None)), evaluator.policy(input("events/list", /*tool_title*/ None)), evaluator.policy(input("calendar_events/create", Some("events/create"))), ], [ AppToolPolicy { enabled: true, approval: AppToolApproval::Approve, }, AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, }, AppToolPolicy { enabled: true, approval: AppToolApproval::Prompt, }, ] ); } #[test] fn evaluator_uses_global_defaults_for_destructive_hints() { let apps_config = AppsConfigToml { default: Some(defaults( /*enabled*/ true, /*destructive_enabled*/ false, /*open_world_enabled*/ true, )), apps: HashMap::new(), }; assert_eq!( policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/create", /*tool_title*/ None, Some(true), /*open_world_hint*/ None, /*managed_approval*/ None, ), AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[test] fn evaluator_defaults_missing_destructive_hint_to_true() { let apps_config = AppsConfigToml { default: Some(defaults( /*enabled*/ true, /*destructive_enabled*/ false, /*open_world_enabled*/ true, )), apps: HashMap::new(), }; assert_eq!( policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/create", /*tool_title*/ None, /*destructive_hint*/ None, Some(false), /*managed_approval*/ None, ), AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[test] fn evaluator_defaults_missing_open_world_hint_to_true() { let apps_config = AppsConfigToml { default: Some(defaults( /*enabled*/ true, /*destructive_enabled*/ true, /*open_world_enabled*/ false, )), apps: HashMap::new(), }; assert_eq!( policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/create", /*tool_title*/ None, Some(false), /*open_world_hint*/ None, /*managed_approval*/ None, ), AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[test] fn app_enablement_uses_defaults_and_per_app_overrides() { let apps_config = AppsConfigToml { default: Some(defaults( /*enabled*/ false, /*destructive_enabled*/ true, /*open_world_enabled*/ true, )), apps: HashMap::from([( "calendar".to_string(), AppConfig { enabled: true, ..Default::default() }, )]), }; assert_eq!( [ app_is_enabled(&apps_config, Some("calendar")), app_is_enabled(&apps_config, Some("drive")), app_is_enabled(&apps_config, /*connector_id*/ None), ], [true, false, false] ); let evaluator = AppToolPolicyEvaluator::from_parts( Some(apps_config), /*requirements_apps_config*/ None, ); assert_eq!( evaluator.apply_app_enabled_state(vec![ app("calendar", /*enabled*/ false), app("drive", /*enabled*/ true), ]), vec![ app("calendar", /*enabled*/ true), app("drive", /*enabled*/ false), ] ); } #[test] fn app_enablement_preserves_source_state_and_honors_local_and_managed_overrides() { let apps_config = AppsConfigToml { default: None, apps: HashMap::from([ ( "calendar".to_string(), AppConfig { enabled: true, ..Default::default() }, ), ( "drive".to_string(), AppConfig { enabled: true, ..Default::default() }, ), ]), }; let requirements = app_enabled_requirement("drive", /*enabled*/ false); let evaluator = AppToolPolicyEvaluator::from_parts(Some(apps_config), Some(&requirements)); assert_eq!( evaluator.apply_app_enabled_state(vec![ app("calendar", /*enabled*/ false), app("drive", /*enabled*/ true), app("slack", /*enabled*/ false), app("gmail", /*enabled*/ true), ]), vec![ app("calendar", /*enabled*/ true), app("drive", /*enabled*/ false), app("slack", /*enabled*/ false), app("gmail", /*enabled*/ true), ] ); } #[test] fn managed_disable_overrides_enabled_app() { let apps_config = AppsConfigToml { default: None, apps: HashMap::from([( "connector_123123".to_string(), AppConfig { enabled: true, ..Default::default() }, )]), }; let requirements = app_enabled_requirement("connector_123123", /*enabled*/ false); assert_eq!( policy_from_config_parts( Some(&apps_config), Some(&requirements), Some("connector_123123"), "events/list", /*tool_title*/ None, /*destructive_hint*/ None, /*open_world_hint*/ None, ), AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[test] fn managed_enable_does_not_override_disabled_app() { let apps_config = AppsConfigToml { default: None, apps: HashMap::from([( "connector_123123".to_string(), AppConfig { enabled: false, ..Default::default() }, )]), }; let requirements = app_enabled_requirement("connector_123123", /*enabled*/ true); assert_eq!( policy_from_config_parts( Some(&apps_config), Some(&requirements), Some("connector_123123"), "events/list", /*tool_title*/ None, /*destructive_hint*/ None, /*open_world_hint*/ None, ), AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[test] fn managed_disable_applies_without_apps_config() { let requirements = app_enabled_requirement("connector_123123", /*enabled*/ false); assert_eq!( policy_from_config_parts( /*apps_config*/ None, Some(&requirements), Some("connector_123123"), "events/list", /*tool_title*/ None, /*destructive_hint*/ None, /*open_world_hint*/ None, ), AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[test] fn evaluator_honors_default_app_enabled_false() { let apps_config = AppsConfigToml { default: Some(defaults( /*enabled*/ false, /*destructive_enabled*/ true, /*open_world_enabled*/ true, )), apps: HashMap::new(), }; assert_eq!( policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/list", /*tool_title*/ None, /*destructive_hint*/ None, /*open_world_hint*/ None, /*managed_approval*/ None, ), AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[test] fn evaluator_allows_per_app_enable_when_default_is_disabled() { let apps_config = AppsConfigToml { default: Some(defaults( /*enabled*/ false, /*destructive_enabled*/ true, /*open_world_enabled*/ true, )), apps: HashMap::from([( "calendar".to_string(), AppConfig { enabled: true, ..Default::default() }, )]), }; assert_eq!( policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/list", /*tool_title*/ None, /*destructive_hint*/ None, /*open_world_hint*/ None, /*managed_approval*/ None, ), AppToolPolicy::default() ); } #[test] fn evaluator_uses_managed_approval_without_apps_config() { assert_eq!( policy_from_apps_config( /*apps_config*/ None, Some("calendar"), "events/list", /*tool_title*/ None, /*destructive_hint*/ None, /*open_world_hint*/ None, Some(AppToolApproval::Approve), ), AppToolPolicy { enabled: true, approval: AppToolApproval::Approve, } ); } #[test] fn managed_approval_uses_raw_tool_name() { let requirements = app_tool_requirements( "connector_123123", "calendar/list_events", AppToolApproval::Approve, ); assert_eq!( [ policy_from_config_parts( /*apps_config*/ None, Some(&requirements), Some("connector_123123"), "calendar/list_events", /*tool_title*/ None, /*destructive_hint*/ None, /*open_world_hint*/ None, ), policy_from_config_parts( /*apps_config*/ None, Some(&requirements), Some("connector_123123"), "calendar/create_event", Some("calendar/list_events"), /*destructive_hint*/ None, /*open_world_hint*/ None, ), ], [ AppToolPolicy { enabled: true, approval: AppToolApproval::Approve, }, AppToolPolicy::default(), ] ); } #[test] fn managed_approval_overrides_user_tool_approval() { let apps_config = AppsConfigToml { default: None, apps: HashMap::from([( "connector_123123".to_string(), AppConfig { enabled: true, tools: Some(AppToolsConfig { tools: HashMap::from([( "calendar/list_events".to_string(), AppToolConfig { enabled: None, approval_mode: Some(AppToolApproval::Prompt), }, )]), }), ..Default::default() }, )]), }; let requirements = app_tool_requirements( "connector_123123", "calendar/list_events", AppToolApproval::Approve, ); assert_eq!( policy_from_config_parts( Some(&apps_config), Some(&requirements), Some("connector_123123"), "calendar/list_events", /*tool_title*/ None, /*destructive_hint*/ None, /*open_world_hint*/ None, ), AppToolPolicy { enabled: true, approval: AppToolApproval::Approve, } ); } #[test] fn link_privacy_mode_overrides_app_default_and_preserves_tool_settings() { let apps_config = serde_json::from_value(serde_json::json!({ "calendar": { "default_tools_approval_mode": "auto", "tools": { "events/create": { "approval_mode": "writes" } }, "links": { "link_calendar": { "default_tools_approval_mode": "approve" }, "link_other": { "default_tools_approval_mode": "prompt" }, "link_without_privacy": {}, }, }, "drive": { "links": { "link_drive": { "default_tools_approval_mode": "prompt" } }, }, "without_links": { "default_tools_approval_mode": "writes", }, "empty_links": { "default_tools_approval_mode": "prompt", "links": {}, }, })) .expect("apps config"); let evaluator = AppToolPolicyEvaluator::from_parts( Some(apps_config), /*requirements_apps_config*/ None, ); for (link_id, approval) in [ (Some("link_calendar"), AppToolApproval::Approve), (Some("link_other"), AppToolApproval::Prompt), (Some("link_without_privacy"), AppToolApproval::Auto), (Some("link_drive"), AppToolApproval::Auto), (None, AppToolApproval::Auto), ] { assert_eq!( evaluator.policy(AppToolPolicyInput { link_id, ..input("events/list", /*tool_title*/ None) }), AppToolPolicy { enabled: true, approval, } ); } assert_eq!( evaluator.policy(AppToolPolicyInput { link_id: Some("link_calendar"), ..input("events/create", /*tool_title*/ None) }), AppToolPolicy { enabled: true, approval: AppToolApproval::Writes, } ); for (connector_id, approval) in [ ("without_links", AppToolApproval::Writes), ("empty_links", AppToolApproval::Prompt), ] { assert_eq!( evaluator.policy(AppToolPolicyInput { connector_id: Some(connector_id), link_id: Some("link_calendar"), ..input("events/list", /*tool_title*/ None) }), AppToolPolicy { enabled: true, approval, } ); } } #[test] fn link_privacy_mode_preserves_managed_connector_requirements() { let apps_config = serde_json::from_value(serde_json::json!({ "calendar": { "links": { "link_calendar": { "default_tools_approval_mode": "approve" } }, }, })) .expect("apps config"); let mut requirements = app_tool_requirements("calendar", "events/create", AppToolApproval::Prompt); requirements .apps .get_mut("calendar") .expect("calendar requirement") .enabled = Some(false); let evaluator = AppToolPolicyEvaluator::from_parts(Some(apps_config), Some(&requirements)); assert_eq!( evaluator.policy(AppToolPolicyInput { link_id: Some("link_calendar"), ..input("events/create", /*tool_title*/ None) }), AppToolPolicy { enabled: false, approval: AppToolApproval::Prompt, } ); } #[test] fn per_tool_enable_overrides_app_level_hints() { let apps_config = AppsConfigToml { default: None, apps: HashMap::from([( "calendar".to_string(), AppConfig { enabled: true, destructive_enabled: Some(false), open_world_enabled: Some(false), tools: Some(AppToolsConfig { tools: HashMap::from([( "events/create".to_string(), AppToolConfig { enabled: Some(true), approval_mode: None, }, )]), }), ..Default::default() }, )]), }; assert_eq!( policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/create", /*tool_title*/ None, Some(true), Some(true), /*managed_approval*/ None, ), AppToolPolicy::default() ); } #[test] fn default_tools_enable_overrides_app_level_hints() { let mut app = AppConfig { enabled: true, destructive_enabled: Some(false), open_world_enabled: Some(false), default_tools_enabled: Some(true), ..Default::default() }; let apps_config = |app: AppConfig| AppsConfigToml { default: None, apps: HashMap::from([("calendar".to_string(), app)]), }; let enabled_policy = policy_from_apps_config( Some(&apps_config(app.clone())), Some("calendar"), "events/create", /*tool_title*/ None, Some(true), Some(true), /*managed_approval*/ None, ); app.destructive_enabled = Some(true); app.open_world_enabled = Some(true); app.default_tools_enabled = Some(false); app.default_tools_approval_mode = Some(AppToolApproval::Approve); let disabled_policy = policy_from_apps_config( Some(&apps_config(app)), Some("calendar"), "events/list", /*tool_title*/ None, /*destructive_hint*/ None, /*open_world_hint*/ None, /*managed_approval*/ None, ); assert_eq!( [enabled_policy, disabled_policy], [ AppToolPolicy::default(), AppToolPolicy { enabled: false, approval: AppToolApproval::Approve, }, ] ); } #[test] fn evaluator_uses_apps_default_tools_approval_mode_only_with_connector_id() { let apps_config = AppsConfigToml { default: Some(AppsDefaultConfig { default_tools_approval_mode: Some(AppToolApproval::Prompt), ..defaults( /*enabled*/ true, /*destructive_enabled*/ true, /*open_world_enabled*/ true, ) }), apps: HashMap::new(), }; assert_eq!( [ policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/list", /*tool_title*/ None, /*destructive_hint*/ None, /*open_world_hint*/ None, /*managed_approval*/ None, ), policy_from_apps_config( Some(&apps_config), /*connector_id*/ None, "events/list", /*tool_title*/ None, /*destructive_hint*/ None, /*open_world_hint*/ None, /*managed_approval*/ None, ), ], [ AppToolPolicy { enabled: true, approval: AppToolApproval::Prompt, }, AppToolPolicy::default(), ] ); } #[test] fn evaluator_prefers_app_default_tools_approval_mode_over_apps_default() { let apps_config = AppsConfigToml { default: Some(AppsDefaultConfig { default_tools_approval_mode: Some(AppToolApproval::Approve), ..defaults( /*enabled*/ true, /*destructive_enabled*/ true, /*open_world_enabled*/ true, ) }), apps: HashMap::from([( "calendar".to_string(), AppConfig { enabled: true, default_tools_approval_mode: Some(AppToolApproval::Prompt), tools: Some(AppToolsConfig { tools: HashMap::new(), }), ..Default::default() }, )]), }; assert_eq!( policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/list", /*tool_title*/ None, /*destructive_hint*/ None, /*open_world_hint*/ None, /*managed_approval*/ None, ), AppToolPolicy { enabled: true, approval: AppToolApproval::Prompt, } ); } #[test] fn evaluator_matches_tool_title_for_user_config() { let apps_config = AppsConfigToml { default: None, apps: HashMap::from([( "calendar".to_string(), AppConfig { enabled: true, destructive_enabled: Some(false), open_world_enabled: Some(false), default_tools_approval_mode: Some(AppToolApproval::Auto), default_tools_enabled: Some(false), tools: Some(AppToolsConfig { tools: HashMap::from([( "events/create".to_string(), AppToolConfig { enabled: Some(true), approval_mode: Some(AppToolApproval::Approve), }, )]), }), ..Default::default() }, )]), }; assert_eq!( policy_from_apps_config( Some(&apps_config), Some("calendar"), "calendar_events/create", Some("events/create"), Some(true), Some(true), /*managed_approval*/ None, ), AppToolPolicy { enabled: true, approval: AppToolApproval::Approve, } ); } fn input<'a>(tool_name: &'a str, tool_title: Option<&'a str>) -> AppToolPolicyInput<'a> { AppToolPolicyInput { connector_id: Some("calendar"), link_id: None, tool_name, tool_title, destructive_hint: Some(true), open_world_hint: Some(true), } } fn app(id: &str, enabled: bool) -> AppInfo { AppInfo { id: id.to_string(), name: id.to_string(), description: None, logo_url: None, logo_url_dark: None, icon_assets: None, icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, labels: None, install_url: None, is_accessible: true, is_enabled: enabled, plugin_display_names: Vec::new(), } } fn policy_from_apps_config( apps_config: Option<&AppsConfigToml>, connector_id: Option<&str>, tool_name: &str, tool_title: Option<&str>, destructive_hint: Option, open_world_hint: Option, managed_approval: Option, ) -> AppToolPolicy { let requirements = managed_approval.map(|approval| { app_tool_requirements( connector_id.expect("managed approval requires a connector id"), tool_name, approval, ) }); policy_from_config_parts( apps_config, requirements.as_ref(), connector_id, tool_name, tool_title, destructive_hint, open_world_hint, ) } fn policy_from_config_parts( apps_config: Option<&AppsConfigToml>, requirements_apps_config: Option<&AppsRequirementsToml>, connector_id: Option<&str>, tool_name: &str, tool_title: Option<&str>, destructive_hint: Option, open_world_hint: Option, ) -> AppToolPolicy { let requirements = ConfigRequirementsToml { apps: requirements_apps_config.cloned(), ..Default::default() }; let config_layer_stack = ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements) .expect("config layer stack"); let config_layer_stack = if let Some(apps_config) = apps_config { let mut user_config = TomlValue::Table(Default::default()); user_config .as_table_mut() .expect("user config table") .insert( "apps".to_string(), TomlValue::try_from(apps_config).expect("serialize apps config"), ); let config_toml_path = AbsolutePathBuf::try_from(std::env::temp_dir().join(CONFIG_TOML_FILE)) .expect("absolute config path"); config_layer_stack .with_user_config(&config_toml_path, user_config) .expect("apps user config should be valid") } else { config_layer_stack }; AppToolPolicyEvaluator::new(&config_layer_stack).policy(AppToolPolicyInput { connector_id, link_id: None, tool_name, tool_title, destructive_hint, open_world_hint, }) } fn app_enabled_requirement(app_id: &str, enabled: bool) -> AppsRequirementsToml { AppsRequirementsToml { apps: BTreeMap::from([( app_id.to_string(), AppRequirementToml { enabled: Some(enabled), tools: None, }, )]), } } fn app_tool_requirements( app_id: &str, tool_name: &str, approval_mode: AppToolApproval, ) -> AppsRequirementsToml { AppsRequirementsToml { apps: BTreeMap::from([( app_id.to_string(), AppRequirementToml { enabled: None, tools: Some(AppToolsRequirementsToml { tools: BTreeMap::from([( tool_name.to_string(), AppToolRequirementToml { approval_mode: Some(approval_mode), ..Default::default() }, )]), }), }, )]), } } fn defaults( enabled: bool, destructive_enabled: bool, open_world_enabled: bool, ) -> AppsDefaultConfig { AppsDefaultConfig { enabled, approvals_reviewer: None, destructive_enabled, open_world_enabled, default_tools_approval_mode: None, } }