use super::{ team::{DiscoverableTeam, MembershipRole, Team, TeamMember}, user_profiles::UserProfileWithUID, user_workspaces::WorkspacesMetadataResponse, workspace::{ AIAutonomyPolicy, AddonCreditsSettings, AdminEnablementSetting, AiAutonomySettings, AiPermissionsSettings, AmbientAgentsPolicy, BillingMetadata, CloudConversationStorageSettings, CodebaseContextSettings, CustomerType, DelinquencyStatus, EmailInvite, EnterpriseSecretRegex, HostEnablementSetting, InstanceShape, InviteLinkDomainRestriction, LinkSharingSettings, LlmSettings, SandboxedAgentSettings, SecretRedactionSettings, SessionSharingPolicy, SharedNotebooksPolicy, SharedWorkflowsPolicy, TelemetryDataCollectionPolicy, TelemetrySettings, Tier, UgcCollectionEnablementSetting, UgcCollectionSettings, UgcDataCollectionPolicy, UsageBasedPricingPolicy, WarpAiPolicy, Workspace, WorkspaceInviteCode, WorkspaceMember, WorkspaceMemberUsageInfo, WorkspaceSettings, WorkspaceSizePolicy, }, }; use crate::{ ai::blocklist::usage::conversation_usage_view::ConversationUsageInfo, ai::execution_profiles::{ActionPermission, ComputerUsePermission, WriteToPtyPermission}, ai::{BonusGrant, BonusGrantScope}, auth::UserUid, cloud_object::{ServerAIExecutionProfile, ServerAIFact}, report_error, server::experiments::ServerExperiment, server::ids::ServerId, settings::AgentModeCommandExecutionPredicate, workspaces::workspace::{ AiOverages, BonusGrantsPurchased, ByoApiKeyPolicy, CodebaseContextPolicy, EnterpriseCreditsAutoReloadPolicy, EnterprisePayAsYouGoPolicy, MultiAdminPolicy, PurchaseAddOnCreditsPolicy, UsageBasedPricingSettings, }, }; use crate::{ cloud_object::{ ServerAmbientAgentEnvironment, ServerCloudAgentConfig, ServerCloudObject, ServerEnvVarCollection, ServerFolder, ServerMCPServer, ServerNotebook, ServerPreference, ServerScheduledAmbientAgent, ServerTemplatableMCPServer, ServerWorkflow, ServerWorkflowEnum, }, convert_to_server_experiment, server::cloud_objects::listener::ObjectUpdateMessage, }; use anyhow::{anyhow, bail}; use galaxy_graphql::workspace::AddonCreditsSettings as GqlAddonCreditsSettings; use galaxy_graphql::{ billing::{ AiAutonomyPolicy as GqlAiAutonomyPolicy, AmbientAgentsPolicy as GqlAmbientAgentsPolicy, BillingMetadata as GqlBillingMetadata, BonusGrant as GqlBonusGrant, ByoApiKeyPolicy as GqlByoApiKeyPolicy, CodebaseContextPolicy as GqlCodebaseContextPolicy, CustomerType as GqlCustomerType, DelinquencyStatus as GqlDelinquencyStatus, EnterpriseCreditsAutoReloadPolicy as GqlEnterpriseCreditsAutoReloadPolicy, EnterprisePayAsYouGoPolicy as GqlEnterprisePayAsYouGoPolicy, InstanceShape as GqlInstanceShape, MultiAdminPolicy as GqlMultiAdminPolicy, PurchaseAddOnCreditsPolicy as GqlPurchaseAddOnCreditsPolicy, ServiceAgreementType, SessionSharingPolicy as GqlSessionSharingPolicy, SharedNotebooksPolicy as GqlSharedNotebooksPolicy, SharedWorkflowsPolicy as GqlSharedWorkflowsPolicy, StripeSubscriptionPlan, TeamSizePolicy as GqlTeamSizePolicy, TelemetryDataCollectionPolicy as GqlTelemetryDataCollectionPolicy, Tier as GqlTier, UgcDataCollectionPolicy as GqlUgcDataCollectionPolicy, UsageBasedPricingPolicy as GqlUsageBasedPricingPolicy, WarpAiPolicy as GqlWarpAiPolicy, }, object::CloudObjectWithDescendants, queries::{ get_conversation_usage as gql_usage, get_workspaces_metadata_for_user::User as GqlUser, }, subscriptions::get_warp_drive_updates::WarpDriveUpdate, user::{DiscoverableTeamData as GqlDiscoverableTeamData, PublicUserProfile}, workspace::{ AdminEnablementSetting as GqlAdminEnablementSetting, AiAutonomyValue as GqlAiAutonomyValue, AiPermissionsSettings as GqlAiPermissionsSettings, ComputerUseAutonomyValue as GqlComputerUseAutonomyValue, EmailInvite as GqlEmailInvite, HostEnablementSetting as GqlHostEnablementSetting, InviteLinkDomainRestriction as GqlInviteLinkDomainRestriction, MembershipRole as GqlMembershipRole, Team as GqlTeam, TeamMember as GqlTeamMember, UgcCollectionEnablementSetting as GqlUgcCollectionEnablementSetting, Workspace as GqlWorkspace, WorkspaceMember as GqlWorkspaceMember, WorkspaceMemberUsageInfo as GqlWorkspaceMemberUsageInfo, WorkspaceSettings as GqlWorkspaceSettings, WriteToPtyAutonomyValue as GqlWriteToPtyAutonomyValue, }, }; use regex::Regex; use std::path::PathBuf; pub const PLACEHOLDER_WORKSPACE_UID: &str = "NOT_A_REAL_WORKSPACE_UID"; impl From for TeamMember { fn from(gql_team_member: GqlTeamMember) -> TeamMember { Self { uid: UserUid::new(&gql_team_member.uid.into_inner()), email: gql_team_member.email, role: gql_team_member.role.into(), } } } impl From for MembershipRole { fn from(role: GqlMembershipRole) -> Self { match role { GqlMembershipRole::Owner => MembershipRole::Owner, GqlMembershipRole::Admin => MembershipRole::Admin, GqlMembershipRole::User => MembershipRole::User, GqlMembershipRole::Unknown => { report_error!(anyhow!( "Invalid MembershipRole from server; treating as User" )); MembershipRole::User } } } } impl From for GqlMembershipRole { fn from(role: MembershipRole) -> Self { match role { MembershipRole::Owner => GqlMembershipRole::Owner, MembershipRole::Admin => GqlMembershipRole::Admin, MembershipRole::User => GqlMembershipRole::User, } } } impl From for WorkspaceMemberUsageInfo { fn from( gql_workspace_member_usage_info: GqlWorkspaceMemberUsageInfo, ) -> WorkspaceMemberUsageInfo { Self { request_limit: gql_workspace_member_usage_info.request_limit, requests_used_since_last_refresh: gql_workspace_member_usage_info .requests_used_since_last_refresh, is_unlimited: gql_workspace_member_usage_info.is_unlimited, is_request_limit_prorated: gql_workspace_member_usage_info.is_request_limit_prorated, } } } impl From for WorkspaceMember { fn from(gql_workspace_member: GqlWorkspaceMember) -> WorkspaceMember { Self { uid: UserUid::new(&gql_workspace_member.uid.into_inner()), email: gql_workspace_member.email, role: gql_workspace_member.role.into(), usage_info: gql_workspace_member.usage_info.into(), } } } impl From for EmailInvite { fn from(gql_email_invite: GqlEmailInvite) -> EmailInvite { Self { invitee_email: gql_email_invite.email, expired: gql_email_invite.expired, } } } impl From for InviteLinkDomainRestriction { fn from( gql_invite_link_domain_restriction: GqlInviteLinkDomainRestriction, ) -> InviteLinkDomainRestriction { InviteLinkDomainRestriction { uid: ServerId::from_string_lossy(gql_invite_link_domain_restriction.uid.inner()), domain: gql_invite_link_domain_restriction.domain, } } } impl From for WarpAiPolicy { fn from(gql_warp_ai_policy: GqlWarpAiPolicy) -> WarpAiPolicy { Self { limit: i64::from(gql_warp_ai_policy.limit), is_code_suggestions_toggleable: gql_warp_ai_policy.is_code_suggestions_toggleable, is_prompt_suggestions_toggleable: gql_warp_ai_policy.is_prompt_suggestions_toggleable, is_next_command_enabled: gql_warp_ai_policy.is_next_command_enabled, is_voice_enabled: gql_warp_ai_policy.is_voice_enabled, } } } impl From for WorkspaceSizePolicy { fn from(gql_workspace_size_policy: GqlTeamSizePolicy) -> WorkspaceSizePolicy { Self { is_unlimited: gql_workspace_size_policy.is_unlimited, limit: i64::from(gql_workspace_size_policy.limit), } } } impl From for SharedNotebooksPolicy { fn from(gql_shared_notebooks_policy: GqlSharedNotebooksPolicy) -> SharedNotebooksPolicy { Self { is_unlimited: gql_shared_notebooks_policy.is_unlimited, limit: i64::from(gql_shared_notebooks_policy.limit), } } } impl From for SharedWorkflowsPolicy { fn from(gql_shared_workflows_policy: GqlSharedWorkflowsPolicy) -> SharedWorkflowsPolicy { Self { is_unlimited: gql_shared_workflows_policy.is_unlimited, limit: i64::from(gql_shared_workflows_policy.limit), } } } impl From for SessionSharingPolicy { fn from(gql_session_sharing_policy: GqlSessionSharingPolicy) -> SessionSharingPolicy { Self { is_enabled: gql_session_sharing_policy.enabled, max_session_size: u64::try_from(gql_session_sharing_policy.max_session_bytes_size) .unwrap_or_default(), } } } impl From for AIAutonomyPolicy { fn from(gql_ai_autonomy_policy: GqlAiAutonomyPolicy) -> AIAutonomyPolicy { Self { is_enabled: gql_ai_autonomy_policy.enabled, toggleable: gql_ai_autonomy_policy.toggleable, } } } impl From for UgcCollectionEnablementSetting { fn from( gql_ugc_collection_enablement_setting: GqlUgcCollectionEnablementSetting, ) -> UgcCollectionEnablementSetting { match gql_ugc_collection_enablement_setting { GqlUgcCollectionEnablementSetting::Disable => UgcCollectionEnablementSetting::Disable, GqlUgcCollectionEnablementSetting::Enable => UgcCollectionEnablementSetting::Enable, GqlUgcCollectionEnablementSetting::RespectUserSetting => { UgcCollectionEnablementSetting::RespectUserSetting } GqlUgcCollectionEnablementSetting::Other(value) => { report_error!(anyhow!( "Invalid UgcCollectionEnablementSetting '{value}'. Make sure to update client GraphQL types!" )); UgcCollectionEnablementSetting::RespectUserSetting } } } } impl From<&gql_usage::ConversationUsage> for ConversationUsageInfo { fn from(gql: &gql_usage::ConversationUsage) -> Self { let persistence::model::ConversationUsageMetadata { credits_spent, token_usage: models, tool_usage_metadata: tool, context_window_usage, .. } = (&gql.usage_metadata).into(); ConversationUsageInfo { credits_spent, credits_spent_for_last_block: None, tool_calls: tool.total_tool_calls(), models, context_window_usage, files_changed: tool.apply_file_diff_stats.files_changed, lines_added: tool.apply_file_diff_stats.lines_added, lines_removed: tool.apply_file_diff_stats.lines_removed, commands_executed: tool.run_command_stats.commands_executed, total_input_tokens: 0, total_output_tokens: 0, total_cache_read_tokens: 0, total_cache_write_tokens: 0, estimated_cost_cents: 0.0, } } } impl From for AdminEnablementSetting { fn from(gql_admin_enablement_setting: GqlAdminEnablementSetting) -> AdminEnablementSetting { match gql_admin_enablement_setting { GqlAdminEnablementSetting::Disable => AdminEnablementSetting::Disable, GqlAdminEnablementSetting::Enable => AdminEnablementSetting::Enable, GqlAdminEnablementSetting::RespectUserSetting => { AdminEnablementSetting::RespectUserSetting } GqlAdminEnablementSetting::Other(value) => { report_error!(anyhow!( "Invalid AdminEnablementSetting '{value}'. Make sure to update client GraphQL types!" )); AdminEnablementSetting::RespectUserSetting } } } } impl From for HostEnablementSetting { fn from(gql_host_enablement_setting: GqlHostEnablementSetting) -> HostEnablementSetting { match gql_host_enablement_setting { GqlHostEnablementSetting::Enforce => HostEnablementSetting::Enforce, GqlHostEnablementSetting::RespectUserSetting => { HostEnablementSetting::RespectUserSetting } GqlHostEnablementSetting::Other(value) => { report_error!(anyhow!( "Invalid HostEnablementSetting '{value}'. Make sure to update client GraphQL types!" )); HostEnablementSetting::RespectUserSetting } } } } impl From<&GqlAiPermissionsSettings> for AiPermissionsSettings { fn from(gql_ai_permissions_settings: &GqlAiPermissionsSettings) -> AiPermissionsSettings { Self { allow_ai_in_remote_sessions: gql_ai_permissions_settings.allow_ai_in_remote_sessions, remote_session_regex_list: gql_ai_permissions_settings .remote_session_regex_list .iter() .filter_map(|r| { let regex = Regex::new(r); match regex { Ok(regex) => Some(regex), Err(_) => { log::error!("Invalid regex pattern for remote session detection: {r}"); None } } }) .collect(), } } } impl From for UgcDataCollectionPolicy { fn from(gql_ugc_data_collection_policy: GqlUgcDataCollectionPolicy) -> UgcDataCollectionPolicy { Self { default_setting: UgcCollectionEnablementSetting::from( gql_ugc_data_collection_policy.default_setting, ), toggleable: gql_ugc_data_collection_policy.toggleable, } } } impl From for TelemetryDataCollectionPolicy { fn from( gql_telemetry_data_collection_policy: GqlTelemetryDataCollectionPolicy, ) -> TelemetryDataCollectionPolicy { Self { default: gql_telemetry_data_collection_policy.default, toggleable: gql_telemetry_data_collection_policy.toggleable, } } } impl From for UsageBasedPricingPolicy { fn from(gql_usage_based_pricing_policy: GqlUsageBasedPricingPolicy) -> UsageBasedPricingPolicy { Self { toggleable: gql_usage_based_pricing_policy.toggleable, } } } impl From for AddonCreditsSettings { fn from(gql_settings: GqlAddonCreditsSettings) -> AddonCreditsSettings { Self { auto_reload_enabled: gql_settings.auto_reload_enabled, max_monthly_spend_cents: gql_settings.max_monthly_spend_cents, selected_auto_reload_credit_denomination: gql_settings .selected_auto_reload_credit_denomination, } } } impl From for CodebaseContextPolicy { fn from(gql_codebase_context_policy: GqlCodebaseContextPolicy) -> CodebaseContextPolicy { Self { toggleable: gql_codebase_context_policy.toggleable, index_limit: if gql_codebase_context_policy.is_unlimited_indices { None } else { Some(gql_codebase_context_policy.max_indices as u32) }, max_files_per_repo: gql_codebase_context_policy.max_files_per_repo as u32, } } } impl From for ByoApiKeyPolicy { fn from(gql_byo_api_key_policy: GqlByoApiKeyPolicy) -> ByoApiKeyPolicy { Self { enabled: gql_byo_api_key_policy.enabled, } } } impl From for PurchaseAddOnCreditsPolicy { fn from( gql_purchase_add_on_credits_policy: GqlPurchaseAddOnCreditsPolicy, ) -> PurchaseAddOnCreditsPolicy { Self { enabled: gql_purchase_add_on_credits_policy.enabled, } } } impl From for EnterprisePayAsYouGoPolicy { fn from(gql_policy: GqlEnterprisePayAsYouGoPolicy) -> EnterprisePayAsYouGoPolicy { Self { enabled: gql_policy.enabled, } } } impl From for EnterpriseCreditsAutoReloadPolicy { fn from(gql_policy: GqlEnterpriseCreditsAutoReloadPolicy) -> EnterpriseCreditsAutoReloadPolicy { Self { enabled: gql_policy.enabled, } } } impl From for MultiAdminPolicy { fn from(gql_policy: GqlMultiAdminPolicy) -> MultiAdminPolicy { Self { enabled: gql_policy.enabled, } } } impl From for InstanceShape { fn from(gql_instance_shape: GqlInstanceShape) -> InstanceShape { Self { vcpus: gql_instance_shape.vcpus, memory_gb: gql_instance_shape.memory_gb, } } } impl From for AmbientAgentsPolicy { fn from(gql_policy: GqlAmbientAgentsPolicy) -> AmbientAgentsPolicy { Self { max_concurrent_agents: gql_policy.max_concurrent_agents, instance_shape: gql_policy.instance_shape.map(From::from), } } } impl From for Tier { fn from(gql_tier: GqlTier) -> Tier { Self { name: gql_tier.name, description: gql_tier.description, warp_ai_policy: gql_tier.warp_ai_policy.map(From::from), workspace_size_policy: gql_tier.team_size_policy.map(From::from), shared_notebooks_policy: gql_tier.shared_notebooks_policy.map(From::from), shared_workflows_policy: gql_tier.shared_workflows_policy.map(From::from), session_sharing_policy: gql_tier.session_sharing_policy.map(From::from), ai_autonomy_policy: gql_tier.ai_autonomy_policy.map(From::from), telemetry_data_collection_policy: gql_tier .telemetry_data_collection_policy .map(From::from), ugc_data_collection_policy: gql_tier.ugc_data_collection_policy.map(From::from), usage_based_pricing_policy: gql_tier.usage_based_pricing_policy.map(From::from), codebase_context_policy: gql_tier.codebase_context_policy.map(From::from), byo_api_key_policy: gql_tier.byo_api_key_policy.map(From::from), purchase_add_on_credits_policy: gql_tier.purchase_add_on_credits_policy.map(From::from), enterprise_pay_as_you_go_policy: gql_tier .enterprise_pay_as_you_go_policy .map(From::from), enterprise_credits_auto_reload_policy: gql_tier .enterprise_credits_auto_reload_policy .map(From::from), multi_admin_policy: gql_tier.multi_admin_policy.map(From::from), ambient_agents_policy: gql_tier.ambient_agents_policy.map(From::from), } } } impl From for CustomerType { fn from(gql_customer_type: GqlCustomerType) -> CustomerType { match gql_customer_type { GqlCustomerType::Free => CustomerType::Free, GqlCustomerType::Turbo => CustomerType::Turbo, GqlCustomerType::SelfServe => CustomerType::SelfServe, GqlCustomerType::Prosumer => CustomerType::Prosumer, GqlCustomerType::Legacy => CustomerType::Legacy, GqlCustomerType::Enterprise => CustomerType::Enterprise, GqlCustomerType::Business => CustomerType::Business, GqlCustomerType::Lightspeed => CustomerType::Lightspeed, GqlCustomerType::Build => CustomerType::Build, GqlCustomerType::BuildMax => CustomerType::BuildMax, GqlCustomerType::ProTrial | GqlCustomerType::TeamTrial | GqlCustomerType::Other(_) => { CustomerType::Unknown } } } } impl From for DelinquencyStatus { fn from(gql_delinquency_status: GqlDelinquencyStatus) -> DelinquencyStatus { match gql_delinquency_status { GqlDelinquencyStatus::NoDelinquency => DelinquencyStatus::NoDelinquency, GqlDelinquencyStatus::PastDue => DelinquencyStatus::PastDue, GqlDelinquencyStatus::Unpaid => DelinquencyStatus::Unpaid, GqlDelinquencyStatus::TeamLimitExceeded => DelinquencyStatus::TeamLimitExceeded, GqlDelinquencyStatus::Other(_) => DelinquencyStatus::Unknown, } } } impl BonusGrant { pub fn from_gql_bonus_grant(bonus_grant: GqlBonusGrant, scope: BonusGrantScope) -> Self { Self { created_at: bonus_grant.created_at.utc(), cost_cents: bonus_grant.cost_cents, expiration: bonus_grant.expiration.map(|exp| exp.utc()), grant_type: bonus_grant.grant_type, reason: bonus_grant.reason, user_facing_message: bonus_grant.user_facing_message, request_credits_granted: bonus_grant.request_credits_granted, request_credits_remaining: bonus_grant.request_credits_remaining, scope, } } } impl From for BillingMetadata { fn from(gql_billing_metadata: GqlBillingMetadata) -> BillingMetadata { Self { tier: gql_billing_metadata.tier.into(), customer_type: gql_billing_metadata.customer_type.into(), delinquency_status: gql_billing_metadata.delinquency_status.into(), service_agreements: gql_billing_metadata.service_agreements, ai_overages: gql_billing_metadata.ai_overages.map(|overages| AiOverages { current_monthly_request_cost_cents: overages.current_monthly_request_cost_cents, current_monthly_requests_used: overages.current_monthly_requests_used, current_period_end: overages.current_period_end.utc(), }), } } } impl TryFrom<&BillingMetadata> for StripeSubscriptionPlan { type Error = (); fn try_from(billing_metadata: &BillingMetadata) -> Result { match billing_metadata.customer_type { CustomerType::Turbo => Ok(StripeSubscriptionPlan::Turbo), CustomerType::SelfServe => Ok(StripeSubscriptionPlan::Team), CustomerType::Prosumer => Ok(StripeSubscriptionPlan::Pro), CustomerType::Business => { // Check if this is a legacy Business Plan, or a new Build Business plan based on service agreement type // See: https://github.com/warpdotdev/warp-server/pull/6828#discussion_r2496242091 match billing_metadata .service_agreements .first() .map(|sa| sa.type_.clone()) { Some(ServiceAgreementType::SelfServe) => { Ok(StripeSubscriptionPlan::BuildBusiness) } _ => Ok(StripeSubscriptionPlan::Business), } } CustomerType::Lightspeed => Ok(StripeSubscriptionPlan::Lightspeed), CustomerType::Build => Ok(StripeSubscriptionPlan::Build), CustomerType::BuildMax => Ok(StripeSubscriptionPlan::BuildMax), // legacy customer types we don't support anymore, or customer types that don't get billed via stripe CustomerType::Free | CustomerType::Legacy | CustomerType::Enterprise | CustomerType::Unknown => Err(()), } } } fn convert_gql_ai_autonomy_value_to_action_permission( gql_ai_autonomy_value: GqlAiAutonomyValue, ) -> Option { match gql_ai_autonomy_value { GqlAiAutonomyValue::AgentDecides => Some(ActionPermission::AgentDecides), GqlAiAutonomyValue::AlwaysAllow => Some(ActionPermission::AlwaysAllow), GqlAiAutonomyValue::AlwaysAsk => Some(ActionPermission::AlwaysAsk), GqlAiAutonomyValue::RespectUserSetting => None, GqlAiAutonomyValue::Other(value) => { report_error!(anyhow!( "Invalid AiAutonomyValue '{value}'. Make sure to update client GraphQL types!" )); None } } } fn convert_gql_write_to_pty_autonomy_value_to_write_to_pty_permission( gql_write_to_pty_autonomy_value: GqlWriteToPtyAutonomyValue, ) -> Option { match gql_write_to_pty_autonomy_value { GqlWriteToPtyAutonomyValue::AlwaysAllow => Some(WriteToPtyPermission::AlwaysAllow), GqlWriteToPtyAutonomyValue::AlwaysAsk => Some(WriteToPtyPermission::AlwaysAsk), GqlWriteToPtyAutonomyValue::AskOnFirstWrite => Some(WriteToPtyPermission::AskOnFirstWrite), GqlWriteToPtyAutonomyValue::RespectUserSetting => None, GqlWriteToPtyAutonomyValue::Other(value) => { report_error!(anyhow!( "Invalid WriteToPtyAutonomyValue '{value}'. Make sure to update client GraphQL types!" )); None } } } fn convert_gql_computer_use_autonomy_value_to_computer_use_permission( gql_computer_use_autonomy_value: GqlComputerUseAutonomyValue, ) -> Option { match gql_computer_use_autonomy_value { GqlComputerUseAutonomyValue::Never => Some(ComputerUsePermission::Never), GqlComputerUseAutonomyValue::AlwaysAsk => Some(ComputerUsePermission::AlwaysAsk), GqlComputerUseAutonomyValue::AlwaysAllow => Some(ComputerUsePermission::AlwaysAllow), GqlComputerUseAutonomyValue::RespectUserSetting => None, GqlComputerUseAutonomyValue::Other(value) => { report_error!(anyhow!( "Invalid ComputerUseAutonomyValue '{value}'. Make sure to update client GraphQL types!" )); None } } } trait ToAgentModeCommandExecutionPredicates { fn to_predicates(self) -> Vec; } impl ToAgentModeCommandExecutionPredicates for Vec { fn to_predicates(self) -> Vec { self.into_iter() .filter_map(|pattern| { match AgentModeCommandExecutionPredicate::new_regex(&pattern) { Ok(predicate) => Some(predicate), Err(e) => { report_error!(anyhow!(e).context( "Couldn't parse GQL-provided command regex into AgentModeCommandExecutionPredicate" )); None } } }) .collect() } } trait ToPathBufs { fn to_path_bufs(self) -> Vec; } impl ToPathBufs for Vec { fn to_path_bufs(self) -> Vec { self.into_iter().map(PathBuf::from).collect() } } impl From for crate::ai::llms::LLMModelHost { fn from(gql_host: galaxy_graphql::workspace::LlmModelHost) -> Self { use galaxy_graphql::workspace::LlmModelHost as GqlLlmModelHost; match gql_host { GqlLlmModelHost::DirectApi => Self::DirectApi, GqlLlmModelHost::AwsBedrock => Self::AwsBedrock, GqlLlmModelHost::Other(value) => { report_error!(anyhow!( "Unknown LlmModelHost '{value}'. Make sure to update client GraphQL types!" )); Self::Unknown } } } } impl From for super::workspace::LlmHostSettings { fn from(gql_settings: galaxy_graphql::workspace::LlmHostSettings) -> Self { Self { enabled: gql_settings.enabled, enablement_setting: gql_settings .enablement_setting .map(Into::into) .unwrap_or_default(), } } } impl From for LlmSettings { fn from(gql_settings: galaxy_graphql::workspace::LlmSettings) -> Self { let mut host_configs = std::collections::HashMap::new(); for entry in gql_settings.host_configs { let host: crate::ai::llms::LLMModelHost = entry.host.into(); if host_configs .insert(host.clone(), entry.settings.into()) .is_some() { log::warn!( "Duplicate LLMModelHost entry for {:?}, using latest value", host ); } } Self { enabled: gql_settings.enabled, host_configs, } } } impl From for WorkspaceSettings { fn from(gql_workspace_settings: GqlWorkspaceSettings) -> WorkspaceSettings { Self { llm_settings: gql_workspace_settings.llm_settings.into(), telemetry_settings: TelemetrySettings { force_enabled: gql_workspace_settings.telemetry_settings.force_enabled, }, ugc_collection_settings: UgcCollectionSettings { setting: UgcCollectionEnablementSetting::from( gql_workspace_settings.ugc_collection_settings.setting, ), }, cloud_conversation_storage_settings: CloudConversationStorageSettings { setting: gql_workspace_settings .cloud_conversation_storage_settings .setting .into(), }, ai_permissions_settings: AiPermissionsSettings { allow_ai_in_remote_sessions: gql_workspace_settings .ai_permissions_settings .allow_ai_in_remote_sessions, remote_session_regex_list: gql_workspace_settings .ai_permissions_settings .remote_session_regex_list .iter() .filter_map(|r| { let regex = Regex::new(r); match regex { Ok(regex) => Some(regex), Err(_) => { log::error!( "Invalid regex pattern for remote session detection: {r}" ); None } } }) .collect(), }, link_sharing_settings: LinkSharingSettings { anyone_with_link_sharing_enabled: gql_workspace_settings .link_sharing_settings .anyone_with_link_sharing_enabled, direct_link_sharing_enabled: gql_workspace_settings .link_sharing_settings .direct_link_sharing_enabled, }, secret_redaction_settings: SecretRedactionSettings { enabled: gql_workspace_settings.secret_redaction_settings.enabled, regexes: gql_workspace_settings .secret_redaction_settings .regexes .into_iter() .map(|gql_regex| EnterpriseSecretRegex { pattern: gql_regex.pattern, name: gql_regex.name, }) .collect(), }, is_invite_link_enabled: gql_workspace_settings.is_invite_link_enabled, is_discoverable: gql_workspace_settings.is_discoverable, ai_autonomy_settings: AiAutonomySettings { apply_code_diffs_setting: gql_workspace_settings .ai_autonomy_settings .apply_code_diffs_setting .and_then(convert_gql_ai_autonomy_value_to_action_permission), read_files_setting: gql_workspace_settings .ai_autonomy_settings .read_files_setting .and_then(convert_gql_ai_autonomy_value_to_action_permission), read_files_allowlist: gql_workspace_settings .ai_autonomy_settings .read_files_allowlist .map(|allowlist| allowlist.to_path_bufs()), execute_commands_setting: gql_workspace_settings .ai_autonomy_settings .execute_commands_setting .and_then(convert_gql_ai_autonomy_value_to_action_permission), execute_commands_allowlist: gql_workspace_settings .ai_autonomy_settings .execute_commands_allowlist .map(|allowlist| allowlist.to_predicates()), execute_commands_denylist: gql_workspace_settings .ai_autonomy_settings .execute_commands_denylist .map(|denylist| denylist.to_predicates()), write_to_pty_setting: gql_workspace_settings .ai_autonomy_settings .write_to_pty_setting .and_then(convert_gql_write_to_pty_autonomy_value_to_write_to_pty_permission), computer_use_setting: gql_workspace_settings .ai_autonomy_settings .computer_use_setting .and_then(convert_gql_computer_use_autonomy_value_to_computer_use_permission), }, usage_based_pricing_settings: UsageBasedPricingSettings { enabled: gql_workspace_settings.usage_based_pricing_settings.enabled, max_monthly_spend_cents: gql_workspace_settings .usage_based_pricing_settings .max_monthly_spend_cents .and_then(|cents| { if cents < 0 { report_error!(anyhow!( "Usage-based pricing has a negative max monthly spend of {} cents", cents )); None } else { Some(cents as u32) } }), }, addon_credits_settings: gql_workspace_settings.addon_credits_settings.into(), codebase_context_settings: CodebaseContextSettings { setting: gql_workspace_settings .codebase_context_settings .setting .into(), }, sandboxed_agent_settings: gql_workspace_settings.sandboxed_agent_settings.map(|s| { SandboxedAgentSettings { execute_commands_denylist: s .execute_commands_denylist .map(|denylist| denylist.to_predicates()), } }), enable_warp_attribution: gql_workspace_settings .ambient_agent_settings .map(|s| s.enable_warp_attribution.into()) .unwrap_or_default(), } } } impl Team { pub fn from_gql(gql_workspace: GqlWorkspace, gql_team: GqlTeam) -> Team { Self { // TEAM FIELDS // These fields will persist in the Team rust type even after we finish // rolling out workspaces. uid: ServerId::from_string_lossy(gql_team.uid.inner()), name: gql_team.name.clone(), members: gql_team .members .clone() .into_iter() .map(|gql_member| gql_member.into()) .collect(), // WORKSPACE FIELDS // TODO(skambashi): The fields below are derived from the workspace. We should // remove these from the Team rust type and use the values in the parent // Workspace instead. invite_code: gql_workspace .invite_code .clone() .map(|code| WorkspaceInviteCode { code: code.clone() }), pending_email_invites: gql_workspace .pending_email_invites .clone() .into_iter() .map(|gql_email_invite| gql_email_invite.into()) .collect(), invite_link_domain_restrictions: gql_workspace .invite_link_domain_restrictions .clone() .into_iter() .map(|gql_domain_restriction| gql_domain_restriction.into()) .collect(), billing_metadata: gql_workspace.billing_metadata.clone().into(), stripe_customer_id: gql_workspace .stripe_customer_id .as_ref() .map(|id| id.clone().into_inner()), organization_settings: gql_workspace.settings.clone().into(), is_eligible_for_discovery: gql_workspace.is_eligible_for_discovery, has_billing_history: gql_workspace.has_billing_history, } } } impl From for Workspace { fn from(gql_workspace: GqlWorkspace) -> Workspace { Self { uid: ServerId::from_string_lossy(gql_workspace.uid.inner()).into(), name: gql_workspace.name.clone(), stripe_customer_id: gql_workspace .stripe_customer_id .as_ref() .map(|id| id.clone().into_inner()), teams: gql_workspace .teams .clone() .into_iter() .map(|gql_team| Team::from_gql(gql_workspace.clone(), gql_team)) .collect(), billing_metadata: gql_workspace.billing_metadata.clone().into(), bonus_grants_purchased_this_month: gql_workspace .bonus_grants_info .spending_info .map(|info| BonusGrantsPurchased { total_credits_purchased: info.current_month_credits_purchased, cents_spent: info.current_month_spend_cents, }) .unwrap_or_default(), has_billing_history: gql_workspace.has_billing_history, settings: gql_workspace.settings.clone().into(), invite_code: gql_workspace .invite_code .clone() .map(|code| WorkspaceInviteCode { code: code.clone() }), invite_link_domain_restrictions: gql_workspace .invite_link_domain_restrictions .clone() .into_iter() .map(|gql_domain_restriction| gql_domain_restriction.into()) .collect(), pending_email_invites: gql_workspace .pending_email_invites .clone() .into_iter() .map(|gql_email_invite| gql_email_invite.into()) .collect(), is_eligible_for_discovery: gql_workspace.is_eligible_for_discovery, members: gql_workspace .members .clone() .into_iter() .map(|gql_member| gql_member.into()) .collect(), total_requests_used_since_last_refresh: gql_workspace .total_requests_used_since_last_refresh, } } } impl From for WorkspacesMetadataResponse { fn from(gql_user: GqlUser) -> WorkspacesMetadataResponse { let feature_model_choices = gql_user .workspaces .first() .map(|gql_workspace| gql_workspace.feature_model_choice.clone()); let workspaces: Vec = gql_user .workspaces .clone() .into_iter() .filter(|gql_workspace| { // TODO(skambashi): REV-717: Clean up this code once every user always has // a workspace, and the server no longer returns a placeholder workspace. gql_workspace.uid != PLACEHOLDER_WORKSPACE_UID.into() }) .map(|gql_workspace| gql_workspace.into()) .collect(); let joinable_teams = gql_user .discoverable_teams .clone() .into_iter() .map(|gql_joinable_team| gql_joinable_team.into()) .collect(); let experiments = gql_user .experiments .and_then(|experiments| convert_to_server_experiment!(experiments)); // TODO(skambashi) refactor to return back workspaces, and not teams WorkspacesMetadataResponse { workspaces, joinable_teams, experiments, feature_model_choices, } } } impl From for UserProfileWithUID { fn from(value: PublicUserProfile) -> Self { UserProfileWithUID { firebase_uid: UserUid::new(&value.uid), display_name: value.display_name, email: value.email.unwrap_or_default(), photo_url: value.photo_url.unwrap_or_default(), } } } impl TryFrom for ObjectUpdateMessage { type Error = anyhow::Error; fn try_from(value: WarpDriveUpdate) -> Result { match value { WarpDriveUpdate::ObjectActionOccurred(message) => { Ok(ObjectUpdateMessage::ObjectActionOccurred { history: message.history.try_into()?, }) } WarpDriveUpdate::ObjectContentUpdated(message) => { let server_object = message.object.try_into()?; let last_editor = message.last_editor.map(|e| e.into()); Ok(ObjectUpdateMessage::ObjectContentChanged { server_object: Box::new(server_object), last_editor, }) } WarpDriveUpdate::ObjectDeleted(message) => Ok(ObjectUpdateMessage::ObjectDeleted { object_uid: ServerId::from_string_lossy(message.object_uid.inner()), }), WarpDriveUpdate::ObjectMetadataUpdated(message) => { Ok(ObjectUpdateMessage::ObjectMetadataChanged { metadata: message.metadata.try_into()?, }) } WarpDriveUpdate::ObjectPermissionsUpdated(message) => { Ok(ObjectUpdateMessage::ObjectPermissionsChangedV2 { object_uid: ServerId::from_string_lossy(message.object_uid.inner()), user_profiles: message .user_profiles .into_iter() .flatten() .map(Into::into) .collect(), permissions: message.permissions.try_into()?, }) } WarpDriveUpdate::TeamMembershipsChanged(_) => { Ok(ObjectUpdateMessage::TeamMembershipsChanged) } WarpDriveUpdate::AmbientTaskUpdated(message) => { Ok(ObjectUpdateMessage::AmbientTaskUpdated { task_id: message.task_id.inner().to_string(), timestamp: message.task_updated_ts.utc(), }) } WarpDriveUpdate::Unknown => bail!("Unexpected WarpDriveUpdate variant"), } } } impl TryFrom for ServerFolder { type Error = anyhow::Error; fn try_from(folder: galaxy_graphql::folder::Folder) -> Result { ServerFolder::try_from_graphql_fields( ServerId::from_string_lossy(folder.metadata.uid.inner()), Some(folder.name), folder.metadata.try_into()?, folder.permissions.try_into()?, folder.is_warp_pack, ) } } impl TryFrom for ServerNotebook { type Error = anyhow::Error; fn try_from(notebook: galaxy_graphql::notebook::Notebook) -> Result { ServerNotebook::try_from_graphql_fields( ServerId::from_string_lossy(notebook.metadata.uid.inner()), Some(notebook.title), Some(notebook.data), notebook.ai_document_id, notebook.metadata.try_into()?, notebook.permissions.try_into()?, ) } } impl TryFrom for ServerWorkflow { type Error = anyhow::Error; fn try_from(workflow: galaxy_graphql::workflow::Workflow) -> Result { ServerWorkflow::try_from_graphql_fields( ServerId::from_string_lossy(workflow.metadata.uid.inner()), workflow.data, workflow.metadata.try_into()?, workflow.permissions.try_into()?, ) } } impl TryFrom for ServerEnvVarCollection { type Error = anyhow::Error; fn try_from( gso: galaxy_graphql::generic_string_object::GenericStringObject, ) -> Result { ServerEnvVarCollection::try_from_graphql_fields( ServerId::from_string_lossy(gso.metadata.uid.inner()), Some(gso.serialized_model), gso.metadata.try_into()?, gso.permissions.try_into()?, ) } } impl TryFrom for ServerWorkflowEnum { type Error = anyhow::Error; fn try_from( gso: galaxy_graphql::generic_string_object::GenericStringObject, ) -> Result { ServerWorkflowEnum::try_from_graphql_fields( ServerId::from_string_lossy(gso.metadata.uid.inner()), Some(gso.serialized_model), gso.metadata.try_into()?, gso.permissions.try_into()?, ) } } impl TryFrom for ServerAIFact { type Error = anyhow::Error; fn try_from( gso: galaxy_graphql::generic_string_object::GenericStringObject, ) -> Result { ServerAIFact::try_from_graphql_fields( ServerId::from_string_lossy(gso.metadata.uid.inner()), Some(gso.serialized_model), gso.metadata.try_into()?, gso.permissions.try_into()?, ) } } impl TryFrom for ServerAIExecutionProfile { type Error = anyhow::Error; fn try_from( gso: galaxy_graphql::generic_string_object::GenericStringObject, ) -> Result { ServerAIExecutionProfile::try_from_graphql_fields( ServerId::from_string_lossy(gso.metadata.uid.inner()), Some(gso.serialized_model), gso.metadata.try_into()?, gso.permissions.try_into()?, ) } } impl TryFrom for ServerMCPServer { type Error = anyhow::Error; fn try_from( gso: galaxy_graphql::generic_string_object::GenericStringObject, ) -> Result { ServerMCPServer::try_from_graphql_fields( ServerId::from_string_lossy(gso.metadata.uid.inner()), Some(gso.serialized_model), gso.metadata.try_into()?, gso.permissions.try_into()?, ) } } impl TryFrom for ServerTemplatableMCPServer { type Error = anyhow::Error; fn try_from( gso: galaxy_graphql::generic_string_object::GenericStringObject, ) -> Result { ServerTemplatableMCPServer::try_from_graphql_fields( ServerId::from_string_lossy(gso.metadata.uid.inner()), Some(gso.serialized_model), gso.metadata.try_into()?, gso.permissions.try_into()?, ) } } impl TryFrom for ServerPreference { type Error = anyhow::Error; fn try_from( gso: galaxy_graphql::generic_string_object::GenericStringObject, ) -> Result { ServerPreference::try_from_graphql_fields( ServerId::from_string_lossy(gso.metadata.uid.inner()), Some(gso.serialized_model), gso.metadata.try_into()?, gso.permissions.try_into()?, ) } } impl TryFrom for ServerAmbientAgentEnvironment { type Error = anyhow::Error; fn try_from( gso: galaxy_graphql::generic_string_object::GenericStringObject, ) -> Result { ServerAmbientAgentEnvironment::try_from_graphql_fields( ServerId::from_string_lossy(gso.metadata.uid.inner()), Some(gso.serialized_model), gso.metadata.try_into()?, gso.permissions.try_into()?, ) } } impl TryFrom for ServerScheduledAmbientAgent { type Error = anyhow::Error; fn try_from( gso: galaxy_graphql::generic_string_object::GenericStringObject, ) -> Result { ServerScheduledAmbientAgent::try_from_graphql_fields( ServerId::from_string_lossy(gso.metadata.uid.inner()), Some(gso.serialized_model), gso.metadata.try_into()?, gso.permissions.try_into()?, ) } } impl TryFrom for ServerCloudAgentConfig { type Error = anyhow::Error; fn try_from( gso: galaxy_graphql::generic_string_object::GenericStringObject, ) -> Result { ServerCloudAgentConfig::try_from_graphql_fields( ServerId::from_string_lossy(gso.metadata.uid.inner()), Some(gso.serialized_model), gso.metadata.try_into()?, gso.permissions.try_into()?, ) } } impl TryFrom for ServerCloudObject { type Error = anyhow::Error; fn try_from(value: galaxy_graphql::object::CloudObject) -> Result { match value { galaxy_graphql::object::CloudObject::AIConversation(_) => { Err(anyhow::anyhow!("AIConversation is not a supported object type for this operation")) } galaxy_graphql::object::CloudObject::Folder(folder) => { Ok(ServerCloudObject::Folder(folder.try_into()?)) } galaxy_graphql::object::CloudObject::GenericStringObject(gso) => { match gso.format { galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonEnvVarCollection => { Ok(ServerCloudObject::EnvVarCollection(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonPreference => { Ok(ServerCloudObject::Preference(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonWorkflowEnum => { Ok(ServerCloudObject::WorkflowEnum(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonAIFact => { Ok(ServerCloudObject::AIFact(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonMCPServer => { Ok(ServerCloudObject::MCPServer(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonAIExecutionProfile => { Ok(ServerCloudObject::AIExecutionProfile(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonTemplatableMCPServer => { Ok(ServerCloudObject::TemplatableMCPServer(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonCloudEnvironment => { Ok(ServerCloudObject::AmbientAgentEnvironment(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonScheduledAmbientAgent => { Ok(ServerCloudObject::ScheduledAmbientAgent(gso.try_into()?)) } } } galaxy_graphql::object::CloudObject::Notebook(notebook) => { Ok(ServerCloudObject::Notebook(notebook.try_into()?)) } galaxy_graphql::object::CloudObject::Workflow(workflow) => { Ok(ServerCloudObject::Workflow(Box::new(workflow.try_into()?))) } galaxy_graphql::object::CloudObject::Unknown => { Err(anyhow::anyhow!("Unable to convert cloud object type")) } } } } impl TryFrom for ServerCloudObject { type Error = anyhow::Error; fn try_from(value: CloudObjectWithDescendants) -> Result { match value { CloudObjectWithDescendants::AIConversation(_) => { Err(anyhow::anyhow!("AIConversation is not a supported object type for this operation")) } CloudObjectWithDescendants::FolderWithDescendants(fwd) => { Ok(ServerCloudObject::Folder(fwd.folder.try_into()?)) } CloudObjectWithDescendants::GenericStringObject(gso) => match gso.format { galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonEnvVarCollection => { Ok(ServerCloudObject::EnvVarCollection(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonPreference => { Ok(ServerCloudObject::Preference(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonWorkflowEnum => { Ok(ServerCloudObject::WorkflowEnum(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonAIFact => { Ok(ServerCloudObject::AIFact(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonMCPServer => { Ok(ServerCloudObject::MCPServer(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonAIExecutionProfile => { Ok(ServerCloudObject::AIExecutionProfile(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonTemplatableMCPServer => { Ok(ServerCloudObject::TemplatableMCPServer(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonCloudEnvironment => { Ok(ServerCloudObject::AmbientAgentEnvironment(gso.try_into()?)) } galaxy_graphql::generic_string_object::GenericStringObjectFormat::JsonScheduledAmbientAgent => { Ok(ServerCloudObject::ScheduledAmbientAgent(gso.try_into()?)) } } CloudObjectWithDescendants::Notebook(notebook) => Ok(ServerCloudObject::Notebook(notebook.try_into()?)), CloudObjectWithDescendants::Workflow(workflow) => Ok(ServerCloudObject::Workflow(Box::new(workflow.try_into()?))), CloudObjectWithDescendants::Unknown => Err(anyhow::anyhow!("Unable to convert cloud object with descendants type")), } } } impl From for DiscoverableTeam { fn from(gql_discoverable_team: GqlDiscoverableTeamData) -> DiscoverableTeam { Self { team_uid: gql_discoverable_team.team_uid.into_inner(), num_members: i64::from(gql_discoverable_team.num_members), name: gql_discoverable_team.name, team_accepting_invites: gql_discoverable_team.team_accepting_invites, } } }