first pass of merging in warp (doesn't build)
This commit is contained in:
+253
-464
@@ -1,89 +1,79 @@
|
||||
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;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use regex::Regex;
|
||||
use galaxy_graphql::billing::{
|
||||
AiAutonomyPolicy as GqlAiAutonomyPolicy, AmbientAgentsPolicy as GqlAmbientAgentsPolicy,
|
||||
BillingCycleUsageHistory as GqlBillingCycleUsageHistory, 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,
|
||||
UsageVisibilityGranularity as GqlUsageVisibilityGranularity,
|
||||
UsageVisibilityPolicy as GqlUsageVisibilityPolicy, WarpAiPolicy as GqlWarpAiPolicy,
|
||||
};
|
||||
use galaxy_graphql::queries::get_conversation_usage as gql_usage;
|
||||
use galaxy_graphql::queries::get_workspaces_metadata_for_user::User as GqlUser;
|
||||
use galaxy_graphql::subscriptions::get_warp_drive_updates::WarpDriveUpdate;
|
||||
use galaxy_graphql::user::DiscoverableTeamData as GqlDiscoverableTeamData;
|
||||
use galaxy_graphql::workspace::{
|
||||
AddonCreditsSettings as GqlAddonCreditsSettings,
|
||||
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 super::team::{DiscoverableTeam, MembershipRole, Team, TeamMember};
|
||||
use super::user_workspaces::WorkspacesMetadataResponse;
|
||||
use super::workspace::{
|
||||
AIAutonomyPolicy, AddonCreditsSettings, AdminEnablementSetting, AiAutonomySettings,
|
||||
AiPermissionsSettings, AmbientAgentsPolicy, BillingCycleUsageData, BillingCycleUsageEntry,
|
||||
BillingCycleUsageSummary, BillingMetadata, CloudConversationStorageSettings,
|
||||
CodebaseContextSettings, CustomerType, DelinquencyStatus, EmailInvite, EnterpriseSecretRegex,
|
||||
HostEnablementSetting, InstanceShape, InviteLinkDomainRestriction, LinkSharingSettings,
|
||||
LlmSettings, MaxPriorCycles, SandboxedAgentSettings, SecretRedactionSettings,
|
||||
SessionSharingPolicy, SharedNotebooksPolicy, SharedWorkflowsPolicy,
|
||||
TelemetryDataCollectionPolicy, TelemetrySettings, Tier, UgcCollectionEnablementSetting,
|
||||
UgcCollectionSettings, UgcDataCollectionPolicy, UsageBasedPricingPolicy,
|
||||
UsageVisibilityGranularity, UsageVisibilityPolicy, WarpAiPolicy, Workspace,
|
||||
WorkspaceInviteCode, WorkspaceMember, WorkspaceMemberUsageInfo, WorkspaceSettings,
|
||||
WorkspaceSizePolicy,
|
||||
};
|
||||
use crate::ai::blocklist::usage::conversation_usage_view::ConversationUsageInfo;
|
||||
use crate::ai::execution_profiles::{
|
||||
ActionPermission, ComputerUsePermission, WriteToPtyPermission,
|
||||
};
|
||||
use crate::ai::{BonusGrant, BonusGrantScope};
|
||||
use crate::auth::UserUid;
|
||||
use crate::server::cloud_objects::listener::ObjectUpdateMessage;
|
||||
use crate::server::experiments::ServerExperiment;
|
||||
use crate::server::graphql::schema::object_action_history_from_gql;
|
||||
use crate::server::ids::ServerId;
|
||||
use crate::settings::AgentModeCommandExecutionPredicate;
|
||||
use crate::workspaces::workspace::{
|
||||
AiOverages, BonusGrantsPurchased, ByoApiKeyPolicy, CodebaseContextPolicy,
|
||||
EnterpriseCreditsAutoReloadPolicy, EnterprisePayAsYouGoPolicy, MultiAdminPolicy,
|
||||
PurchaseAddOnCreditsPolicy, UsageBasedPricingSettings,
|
||||
};
|
||||
use crate::{convert_to_server_experiment, report_error};
|
||||
|
||||
pub const PLACEHOLDER_WORKSPACE_UID: &str = "NOT_A_REAL_WORKSPACE_UID";
|
||||
|
||||
impl From<GqlTeamMember> for TeamMember {
|
||||
@@ -174,6 +164,7 @@ impl From<GqlWarpAiPolicy> for WarpAiPolicy {
|
||||
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_git_operations_ai_enabled: gql_warp_ai_policy.is_git_operations_ai_enabled,
|
||||
is_voice_enabled: gql_warp_ai_policy.is_voice_enabled,
|
||||
}
|
||||
}
|
||||
@@ -236,9 +227,12 @@ impl From<GqlUgcCollectionEnablementSetting> for UgcCollectionEnablementSetting
|
||||
UgcCollectionEnablementSetting::RespectUserSetting
|
||||
}
|
||||
GqlUgcCollectionEnablementSetting::Other(value) => {
|
||||
report_error!(anyhow!(
|
||||
"Invalid UgcCollectionEnablementSetting '{value}'. Make sure to update client GraphQL types!"
|
||||
));
|
||||
report_error!(
|
||||
anyhow!(
|
||||
"Invalid UgcCollectionEnablementSetting '{value}'. Make sure to update client GraphQL types!"
|
||||
),
|
||||
galaxy_core::errors::ReportErrorLogMode::OncePerRun
|
||||
);
|
||||
UgcCollectionEnablementSetting::RespectUserSetting
|
||||
}
|
||||
}
|
||||
@@ -248,15 +242,22 @@ impl From<GqlUgcCollectionEnablementSetting> for UgcCollectionEnablementSetting
|
||||
impl From<&gql_usage::ConversationUsage> for ConversationUsageInfo {
|
||||
fn from(gql: &gql_usage::ConversationUsage) -> Self {
|
||||
let persistence::model::ConversationUsageMetadata {
|
||||
credits_spent,
|
||||
platform_credits_spent,
|
||||
token_usage: models,
|
||||
tool_usage_metadata: tool,
|
||||
context_window_usage,
|
||||
context_window_segments,
|
||||
..
|
||||
} = (&gql.usage_metadata).into();
|
||||
ConversationUsageInfo {
|
||||
credits_spent,
|
||||
platform_credits_spent,
|
||||
credits_spent_for_last_block: None,
|
||||
tool_calls: tool.total_tool_calls(),
|
||||
models,
|
||||
context_window_usage,
|
||||
context_window_segments,
|
||||
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,
|
||||
@@ -279,9 +280,12 @@ impl From<GqlAdminEnablementSetting> for AdminEnablementSetting {
|
||||
AdminEnablementSetting::RespectUserSetting
|
||||
}
|
||||
GqlAdminEnablementSetting::Other(value) => {
|
||||
report_error!(anyhow!(
|
||||
"Invalid AdminEnablementSetting '{value}'. Make sure to update client GraphQL types!"
|
||||
));
|
||||
report_error!(
|
||||
anyhow!(
|
||||
"Invalid AdminEnablementSetting '{value}'. Make sure to update client GraphQL types!"
|
||||
),
|
||||
galaxy_core::errors::ReportErrorLogMode::OncePerRun
|
||||
);
|
||||
AdminEnablementSetting::RespectUserSetting
|
||||
}
|
||||
}
|
||||
@@ -296,9 +300,12 @@ impl From<GqlHostEnablementSetting> for HostEnablementSetting {
|
||||
HostEnablementSetting::RespectUserSetting
|
||||
}
|
||||
GqlHostEnablementSetting::Other(value) => {
|
||||
report_error!(anyhow!(
|
||||
"Invalid HostEnablementSetting '{value}'. Make sure to update client GraphQL types!"
|
||||
));
|
||||
report_error!(
|
||||
anyhow!(
|
||||
"Invalid HostEnablementSetting '{value}'. Make sure to update client GraphQL types!"
|
||||
),
|
||||
galaxy_core::errors::ReportErrorLogMode::OncePerRun
|
||||
);
|
||||
HostEnablementSetting::RespectUserSetting
|
||||
}
|
||||
}
|
||||
@@ -441,6 +448,85 @@ impl From<GqlAmbientAgentsPolicy> for AmbientAgentsPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GqlUsageVisibilityGranularity> for UsageVisibilityGranularity {
|
||||
fn from(gql_granularity: GqlUsageVisibilityGranularity) -> UsageVisibilityGranularity {
|
||||
match gql_granularity {
|
||||
GqlUsageVisibilityGranularity::OwnOnly => UsageVisibilityGranularity::OwnOnly,
|
||||
GqlUsageVisibilityGranularity::TeamAggregate => {
|
||||
UsageVisibilityGranularity::TeamAggregate
|
||||
}
|
||||
GqlUsageVisibilityGranularity::PerUserTotals => {
|
||||
UsageVisibilityGranularity::PerUserTotals
|
||||
}
|
||||
GqlUsageVisibilityGranularity::FullBreakdown => {
|
||||
UsageVisibilityGranularity::FullBreakdown
|
||||
}
|
||||
GqlUsageVisibilityGranularity::Other(value) => {
|
||||
report_error!(
|
||||
anyhow!(
|
||||
"Invalid UsageVisibilityGranularity '{value}'. Make sure to update client GraphQL types!"
|
||||
),
|
||||
galaxy_core::errors::ReportErrorLogMode::OncePerRun
|
||||
);
|
||||
// Fail closed to the most restrictive granularity.
|
||||
UsageVisibilityGranularity::OwnOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn from_gql_max_prior_cycles(value: i32) -> MaxPriorCycles {
|
||||
match value {
|
||||
0 => MaxPriorCycles::None,
|
||||
n if n > 0 => MaxPriorCycles::Limited(n as u32),
|
||||
-1 => MaxPriorCycles::Unlimited,
|
||||
other => {
|
||||
report_error!(anyhow!(
|
||||
"Unexpected maxPriorCycles value '{other}' from server; treating as unlimited"
|
||||
));
|
||||
MaxPriorCycles::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GqlUsageVisibilityPolicy> for UsageVisibilityPolicy {
|
||||
fn from(gql_policy: GqlUsageVisibilityPolicy) -> UsageVisibilityPolicy {
|
||||
Self {
|
||||
admin_granularity: gql_policy.admin_granularity.into(),
|
||||
max_prior_cycles: from_gql_max_prior_cycles(gql_policy.max_prior_cycles),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_billing_cycle_usage(history: GqlBillingCycleUsageHistory) -> BillingCycleUsageData {
|
||||
BillingCycleUsageData {
|
||||
current_period_start: history.current_period_start.utc(),
|
||||
current_period_end: history.current_period_end.utc(),
|
||||
summaries: history
|
||||
.summaries
|
||||
.into_iter()
|
||||
.map(|summary| BillingCycleUsageSummary {
|
||||
period_start: summary.period_start.utc(),
|
||||
period_end: summary.period_end.utc(),
|
||||
entries: summary
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|entry| BillingCycleUsageEntry {
|
||||
subject_type: entry.subject_type,
|
||||
subject_uid: entry.subject_uid,
|
||||
subject_display_name: entry.subject_display_name,
|
||||
cost_type: entry.cost_type,
|
||||
usage_bucket: entry.usage_bucket,
|
||||
usage_source: entry.usage_source,
|
||||
credits_used: entry.credits_used,
|
||||
cost_cents: entry.cost_cents,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GqlTier> for Tier {
|
||||
fn from(gql_tier: GqlTier) -> Tier {
|
||||
Self {
|
||||
@@ -468,6 +554,7 @@ impl From<GqlTier> for Tier {
|
||||
.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),
|
||||
usage_visibility_policy: gql_tier.usage_visibility_policy.map(From::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -579,9 +666,12 @@ fn convert_gql_ai_autonomy_value_to_action_permission(
|
||||
GqlAiAutonomyValue::AlwaysAsk => Some(ActionPermission::AlwaysAsk),
|
||||
GqlAiAutonomyValue::RespectUserSetting => None,
|
||||
GqlAiAutonomyValue::Other(value) => {
|
||||
report_error!(anyhow!(
|
||||
"Invalid AiAutonomyValue '{value}'. Make sure to update client GraphQL types!"
|
||||
));
|
||||
report_error!(
|
||||
anyhow!(
|
||||
"Invalid AiAutonomyValue '{value}'. Make sure to update client GraphQL types!"
|
||||
),
|
||||
galaxy_core::errors::ReportErrorLogMode::OncePerRun
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -596,9 +686,12 @@ fn convert_gql_write_to_pty_autonomy_value_to_write_to_pty_permission(
|
||||
GqlWriteToPtyAutonomyValue::AskOnFirstWrite => Some(WriteToPtyPermission::AskOnFirstWrite),
|
||||
GqlWriteToPtyAutonomyValue::RespectUserSetting => None,
|
||||
GqlWriteToPtyAutonomyValue::Other(value) => {
|
||||
report_error!(anyhow!(
|
||||
"Invalid WriteToPtyAutonomyValue '{value}'. Make sure to update client GraphQL types!"
|
||||
));
|
||||
report_error!(
|
||||
anyhow!(
|
||||
"Invalid WriteToPtyAutonomyValue '{value}'. Make sure to update client GraphQL types!"
|
||||
),
|
||||
galaxy_core::errors::ReportErrorLogMode::OncePerRun
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -613,9 +706,12 @@ fn convert_gql_computer_use_autonomy_value_to_computer_use_permission(
|
||||
GqlComputerUseAutonomyValue::AlwaysAllow => Some(ComputerUsePermission::AlwaysAllow),
|
||||
GqlComputerUseAutonomyValue::RespectUserSetting => None,
|
||||
GqlComputerUseAutonomyValue::Other(value) => {
|
||||
report_error!(anyhow!(
|
||||
"Invalid ComputerUseAutonomyValue '{value}'. Make sure to update client GraphQL types!"
|
||||
));
|
||||
report_error!(
|
||||
anyhow!(
|
||||
"Invalid ComputerUseAutonomyValue '{value}'. Make sure to update client GraphQL types!"
|
||||
),
|
||||
galaxy_core::errors::ReportErrorLogMode::OncePerRun
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -658,10 +754,12 @@ impl From<galaxy_graphql::workspace::LlmModelHost> for crate::ai::llms::LLMModel
|
||||
match gql_host {
|
||||
GqlLlmModelHost::DirectApi => Self::DirectApi,
|
||||
GqlLlmModelHost::AwsBedrock => Self::AwsBedrock,
|
||||
GqlLlmModelHost::CustomEndpoint => Self::CustomEndpoint,
|
||||
GqlLlmModelHost::GeminiEnterprise => Self::GeminiEnterprise,
|
||||
GqlLlmModelHost::Other(value) => {
|
||||
report_error!(anyhow!(
|
||||
log::warn!(
|
||||
"Unknown LlmModelHost '{value}'. Make sure to update client GraphQL types!"
|
||||
));
|
||||
);
|
||||
Self::Unknown
|
||||
}
|
||||
}
|
||||
@@ -676,6 +774,8 @@ impl From<galaxy_graphql::workspace::LlmHostSettings> for super::workspace::LlmH
|
||||
.enablement_setting
|
||||
.map(Into::into)
|
||||
.unwrap_or_default(),
|
||||
gcp_audience: gql_settings.gcp_audience,
|
||||
gcp_sa_email: gql_settings.gcp_sa_email,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -831,8 +931,13 @@ impl From<GqlWorkspaceSettings> for WorkspaceSettings {
|
||||
}),
|
||||
enable_warp_attribution: gql_workspace_settings
|
||||
.ambient_agent_settings
|
||||
.map(|s| s.enable_warp_attribution.into())
|
||||
.as_ref()
|
||||
.map(|s| s.enable_warp_attribution.clone().into())
|
||||
.unwrap_or_default(),
|
||||
default_host_slug: gql_workspace_settings
|
||||
.ambient_agent_settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_host_slug.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -908,6 +1013,9 @@ impl From<GqlWorkspace> for Workspace {
|
||||
cents_spent: info.current_month_spend_cents,
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
billing_cycle_usage: gql_workspace
|
||||
.billing_cycle_usage_history
|
||||
.map(convert_billing_cycle_usage),
|
||||
has_billing_history: gql_workspace.has_billing_history,
|
||||
settings: gql_workspace.settings.clone().into(),
|
||||
invite_code: gql_workspace
|
||||
@@ -979,370 +1087,51 @@ impl From<GqlUser> for WorkspacesMetadataResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicUserProfile> 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(),
|
||||
pub fn object_update_message_from_gql(value: WarpDriveUpdate) -> Result<ObjectUpdateMessage> {
|
||||
match value {
|
||||
WarpDriveUpdate::ObjectActionOccurred(message) => {
|
||||
Ok(ObjectUpdateMessage::ObjectActionOccurred {
|
||||
history: object_action_history_from_gql(message.history)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<WarpDriveUpdate> for ObjectUpdateMessage {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: WarpDriveUpdate) -> Result<Self, Self::Error> {
|
||||
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 {
|
||||
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()),
|
||||
}),
|
||||
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"),
|
||||
user_profiles: message
|
||||
.user_profiles
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
permissions: message.permissions.try_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<galaxy_graphql::folder::Folder> for ServerFolder {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(folder: galaxy_graphql::folder::Folder) -> Result<Self, Self::Error> {
|
||||
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<galaxy_graphql::notebook::Notebook> for ServerNotebook {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(notebook: galaxy_graphql::notebook::Notebook) -> Result<Self, Self::Error> {
|
||||
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<galaxy_graphql::workflow::Workflow> for ServerWorkflow {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(workflow: galaxy_graphql::workflow::Workflow) -> Result<Self, Self::Error> {
|
||||
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<galaxy_graphql::generic_string_object::GenericStringObject>
|
||||
for ServerEnvVarCollection
|
||||
{
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(
|
||||
gso: galaxy_graphql::generic_string_object::GenericStringObject,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<galaxy_graphql::generic_string_object::GenericStringObject> for ServerWorkflowEnum {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(
|
||||
gso: galaxy_graphql::generic_string_object::GenericStringObject,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<galaxy_graphql::generic_string_object::GenericStringObject> for ServerAIFact {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(
|
||||
gso: galaxy_graphql::generic_string_object::GenericStringObject,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<galaxy_graphql::generic_string_object::GenericStringObject>
|
||||
for ServerAIExecutionProfile
|
||||
{
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(
|
||||
gso: galaxy_graphql::generic_string_object::GenericStringObject,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<galaxy_graphql::generic_string_object::GenericStringObject> for ServerMCPServer {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(
|
||||
gso: galaxy_graphql::generic_string_object::GenericStringObject,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<galaxy_graphql::generic_string_object::GenericStringObject>
|
||||
for ServerTemplatableMCPServer
|
||||
{
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(
|
||||
gso: galaxy_graphql::generic_string_object::GenericStringObject,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<galaxy_graphql::generic_string_object::GenericStringObject> for ServerPreference {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(
|
||||
gso: galaxy_graphql::generic_string_object::GenericStringObject,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<galaxy_graphql::generic_string_object::GenericStringObject>
|
||||
for ServerAmbientAgentEnvironment
|
||||
{
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(
|
||||
gso: galaxy_graphql::generic_string_object::GenericStringObject,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<galaxy_graphql::generic_string_object::GenericStringObject>
|
||||
for ServerScheduledAmbientAgent
|
||||
{
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(
|
||||
gso: galaxy_graphql::generic_string_object::GenericStringObject,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<galaxy_graphql::generic_string_object::GenericStringObject>
|
||||
for ServerCloudAgentConfig
|
||||
{
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(
|
||||
gso: galaxy_graphql::generic_string_object::GenericStringObject,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<galaxy_graphql::object::CloudObject> for ServerCloudObject {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: galaxy_graphql::object::CloudObject) -> Result<Self, Self::Error> {
|
||||
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"))
|
||||
}
|
||||
WarpDriveUpdate::TeamMembershipsChanged(_) => {
|
||||
Ok(ObjectUpdateMessage::TeamMembershipsChanged)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CloudObjectWithDescendants> for ServerCloudObject {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: CloudObjectWithDescendants) -> Result<Self, Self::Error> {
|
||||
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")),
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use crate::{auth::UserUid, server::ids::ServerId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::workspace::{
|
||||
BillingMetadata, EmailInvite, InviteLinkDomainRestriction, WorkspaceInviteCode,
|
||||
WorkspaceSettings,
|
||||
};
|
||||
use crate::auth::UserUid;
|
||||
use crate::server::ids::ServerId;
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize)]
|
||||
pub enum MembershipRole {
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures::channel::oneshot::{self, Receiver};
|
||||
use futures::stream::AbortHandle;
|
||||
use warpui::r#async::Timer;
|
||||
use warpui::{
|
||||
duration_with_jitter, Entity, ModelContext, ModelHandle, RequestState, SingletonEntity,
|
||||
};
|
||||
|
||||
use super::team_tester::{TeamTesterStatus, TeamTesterStatusEvent};
|
||||
use super::user_workspaces::{
|
||||
CreateTeamResponse, UserWorkspaces, WorkspacesMetadataResponse, WorkspacesMetadataWithPricing,
|
||||
@@ -17,14 +28,6 @@ use crate::server::retry_strategies::{
|
||||
use crate::server::server_api::team::TeamClient;
|
||||
use crate::server::server_api::ServerApiProvider;
|
||||
use crate::{report_error, report_if_error};
|
||||
use anyhow::{Context, Result};
|
||||
use futures::channel::oneshot::{self, Receiver};
|
||||
use futures::stream::AbortHandle;
|
||||
use galaxyui::r#async::Timer;
|
||||
use galaxyui::{duration_with_jitter, RequestState};
|
||||
use galaxyui::{Entity, ModelContext, SingletonEntity};
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub enum TeamUpdateManagerEvent {
|
||||
LeaveSuccess,
|
||||
@@ -77,6 +80,7 @@ impl TeamUpdateManager {
|
||||
|
||||
fn handle_network_status_changed(
|
||||
&mut self,
|
||||
_: ModelHandle<NetworkStatus>,
|
||||
network_status: &NetworkStatusEvent,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
@@ -94,6 +98,7 @@ impl TeamUpdateManager {
|
||||
|
||||
fn handle_team_tester_status_changed(
|
||||
&mut self,
|
||||
_: ModelHandle<TeamTesterStatus>,
|
||||
event: &TeamTesterStatusEvent,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
|
||||
@@ -1,35 +1,25 @@
|
||||
use chrono::Utc;
|
||||
use galaxyui::{AddSingletonModel, App};
|
||||
use cloud_object_client::MockObjectClient;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::{
|
||||
auth::AuthManager,
|
||||
cloud_object::{
|
||||
model::{actions::ObjectActions, persistence::CloudModel},
|
||||
Owner, Revision, ServerMetadata, ServerPermissions, ServerWorkflow,
|
||||
},
|
||||
server::{
|
||||
cloud_objects::update_manager::InitialLoadResponse,
|
||||
ids::SyncId,
|
||||
server_api::{
|
||||
object::MockObjectClient,
|
||||
team::MockTeamClient,
|
||||
workspace::{MockWorkspaceClient, WorkspaceClient},
|
||||
},
|
||||
sync_queue::SyncQueue,
|
||||
telemetry::context_provider::AppTelemetryContextProvider,
|
||||
},
|
||||
settings::PrivacySettings,
|
||||
system::SystemStats,
|
||||
workflows::{workflow::Workflow, CloudWorkflow, CloudWorkflowModel, WorkflowId},
|
||||
workspaces::{
|
||||
team::Team,
|
||||
user_profiles::UserProfiles,
|
||||
workspace::{Workspace, WorkspaceUid},
|
||||
},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::auth::AuthManager;
|
||||
use crate::cloud_object::model::actions::ObjectActions;
|
||||
use crate::cloud_object::model::persistence::CloudModel;
|
||||
use crate::cloud_object::{Owner, Revision, ServerMetadata, ServerPermissions, ServerWorkflow};
|
||||
use crate::server::cloud_objects::update_manager::InitialLoadResponse;
|
||||
use crate::server::ids::SyncId;
|
||||
use crate::server::server_api::team::MockTeamClient;
|
||||
use crate::server::server_api::workspace::{MockWorkspaceClient, WorkspaceClient};
|
||||
use crate::server::sync_queue::SyncQueue;
|
||||
use crate::server::telemetry::context_provider::AppTelemetryContextProvider;
|
||||
use crate::settings::PrivacySettings;
|
||||
use crate::system::SystemStats;
|
||||
use crate::workflows::workflow::Workflow;
|
||||
use crate::workflows::{CloudWorkflow, CloudWorkflowModel, WorkflowId};
|
||||
use crate::workspaces::team::Team;
|
||||
use crate::workspaces::user_profiles::UserProfiles;
|
||||
use crate::workspaces::workspace::{Workspace, WorkspaceUid};
|
||||
|
||||
fn initialize_app(
|
||||
team_client: Arc<dyn TeamClient>,
|
||||
@@ -64,10 +54,10 @@ fn mock_workflow(id: WorkflowId, owner: Owner) -> CloudWorkflow {
|
||||
}
|
||||
|
||||
fn mock_server_workflow(id: WorkflowId, owner: Owner) -> ServerWorkflow {
|
||||
ServerWorkflow {
|
||||
id: SyncId::ServerId(id.into()),
|
||||
model: CloudWorkflowModel::new(Workflow::new("Test Workflow", "echo hello")),
|
||||
metadata: ServerMetadata {
|
||||
ServerWorkflow::new(
|
||||
SyncId::ServerId(id.into()),
|
||||
CloudWorkflowModel::new(Workflow::new("Test Workflow", "echo hello")),
|
||||
ServerMetadata {
|
||||
uid: id.into(),
|
||||
revision: Revision::now(),
|
||||
metadata_last_updated_ts: Utc::now().into(),
|
||||
@@ -78,13 +68,13 @@ fn mock_server_workflow(id: WorkflowId, owner: Owner) -> ServerWorkflow {
|
||||
last_editor_uid: None,
|
||||
current_editor_uid: None,
|
||||
},
|
||||
permissions: ServerPermissions {
|
||||
ServerPermissions {
|
||||
space: owner,
|
||||
permissions_last_updated_ts: Utc::now().into(),
|
||||
anyone_link_sharing: None,
|
||||
guests: vec![],
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,40 +1,21 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use cloud_object_models::UserProfileWithUID;
|
||||
use galaxyui::{Entity, SingletonEntity};
|
||||
use session_sharing_protocol::common::ProfileData;
|
||||
|
||||
use crate::auth::UserUid;
|
||||
|
||||
pub enum UserProfilesEvent {}
|
||||
|
||||
/// Public struct for storing all the UserProfile data that's fed in from either sqlite or the server.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct UserProfileWithUID {
|
||||
pub firebase_uid: UserUid,
|
||||
pub display_name: Option<String>,
|
||||
pub email: String,
|
||||
pub photo_url: String,
|
||||
}
|
||||
|
||||
impl From<ProfileData> for UserProfileWithUID {
|
||||
fn from(data: ProfileData) -> Self {
|
||||
Self {
|
||||
firebase_uid: UserUid::new(&data.firebase_uid),
|
||||
display_name: Some(data.display_name),
|
||||
email: data.email.unwrap_or_default(),
|
||||
photo_url: data.photo_url.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::persistence::model::UserProfile> for UserProfileWithUID {
|
||||
fn from(user_profile: crate::persistence::model::UserProfile) -> Self {
|
||||
UserProfileWithUID {
|
||||
firebase_uid: UserUid::new(&user_profile.firebase_uid),
|
||||
display_name: user_profile.display_name,
|
||||
email: user_profile.email,
|
||||
photo_url: user_profile.photo_url,
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn user_profile_from_persistence(
|
||||
user_profile: crate::persistence::model::UserProfile,
|
||||
) -> UserProfileWithUID {
|
||||
UserProfileWithUID {
|
||||
firebase_uid: UserUid::new(&user_profile.firebase_uid),
|
||||
display_name: user_profile.display_name,
|
||||
email: user_profile.email,
|
||||
photo_url: user_profile.photo_url,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,42 @@
|
||||
use super::{
|
||||
team::{DiscoverableTeam, MembershipRole, Team},
|
||||
workspace::{
|
||||
AdminEnablementSetting, CustomerType, EnterpriseSecretRegex, HostEnablementSetting,
|
||||
UgcCollectionEnablementSetting, Workspace, WorkspaceUid,
|
||||
},
|
||||
};
|
||||
use crate::{
|
||||
ai::llms::LLMModelHost,
|
||||
auth::{AuthStateProvider, UserUid},
|
||||
channel::ChannelState,
|
||||
cloud_object::{
|
||||
model::persistence::CloudModel, CloudObjectEventEntrypoint, ObjectType, Owner, Space,
|
||||
},
|
||||
pricing::PricingInfoModel,
|
||||
report_error,
|
||||
server::{
|
||||
experiments::{ServerExperiment, ServerExperiments, ServerExperimentsEvent},
|
||||
ids::ServerId,
|
||||
server_api::{team::TeamClient, workspace::WorkspaceClient},
|
||||
},
|
||||
settings::{
|
||||
AISettings, AISettingsChangedEvent, CodeSettings, CodeSettingsChangedEvent, PrivacySettings,
|
||||
},
|
||||
workspaces::workspace::{
|
||||
AiAutonomySettings, AiOverages, SandboxedAgentSettings, UsageBasedPricingSettings,
|
||||
},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use galaxy_core::{
|
||||
features::FeatureFlag,
|
||||
settings::{ChangeEventReason, Setting},
|
||||
};
|
||||
use galaxy_graphql::workspace::FeatureModelChoice;
|
||||
use galaxyui::{AppContext, Entity, ModelContext, SingletonEntity, Tracked};
|
||||
use regex::Regex;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use regex::Regex;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::settings::{ChangeEventReason, Setting};
|
||||
use galaxy_graphql::workspace::FeatureModelChoice;
|
||||
use galaxyui::{AppContext, Entity, ModelContext, SingletonEntity, Tracked};
|
||||
|
||||
use super::team::{DiscoverableTeam, MembershipRole, Team};
|
||||
#[cfg(test)]
|
||||
use super::workspace::WorkspaceMemberUsageInfo;
|
||||
use super::workspace::{
|
||||
AdminEnablementSetting, CustomerType, EnterpriseSecretRegex, HostEnablementSetting,
|
||||
UgcCollectionEnablementSetting, Workspace, WorkspaceUid,
|
||||
};
|
||||
use crate::ai::llms::LLMModelHost;
|
||||
use crate::auth::{AuthStateProvider, UserUid};
|
||||
use crate::channel::ChannelState;
|
||||
use crate::cloud_object::model::persistence::CloudModel;
|
||||
use crate::cloud_object::{CloudObjectEventEntrypoint, ObjectType, Owner, Space};
|
||||
use crate::pricing::PricingInfoModel;
|
||||
use crate::report_error;
|
||||
use crate::server::experiments::{ServerExperiment, ServerExperiments, ServerExperimentsEvent};
|
||||
use crate::server::ids::ServerId;
|
||||
use crate::server::server_api::team::TeamClient;
|
||||
use crate::server::server_api::workspace::WorkspaceClient;
|
||||
#[cfg(test)]
|
||||
use crate::server::server_api::{team::MockTeamClient, workspace::MockWorkspaceClient};
|
||||
|
||||
use crate::settings::{
|
||||
AISettings, AISettingsChangedEvent, CodeSettings, CodeSettingsChangedEvent, PrivacySettings,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use crate::workspaces::workspace::{
|
||||
AIAutonomyPolicy, BillingMetadata, WorkspaceMember, WorkspaceSettings,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
use super::workspace::WorkspaceMemberUsageInfo;
|
||||
use crate::workspaces::workspace::{
|
||||
AiAutonomySettings, AiOverages, SandboxedAgentSettings, UsageBasedPricingSettings,
|
||||
};
|
||||
|
||||
const STRIPE_SUBSCRIPTION_INTERVAL_PAGE_PREFIX: &str = "/upgrade";
|
||||
|
||||
@@ -167,22 +158,23 @@ impl UserWorkspaces {
|
||||
current_workspace_uid: Option<WorkspaceUid>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
ctx.subscribe_to_model(&ServerExperiments::handle(ctx), |me, event, ctx| {
|
||||
ctx.subscribe_to_model(&ServerExperiments::handle(ctx), |me, _, event, ctx| {
|
||||
let ServerExperimentsEvent::ExperimentsUpdated = event;
|
||||
me.update_session_sharing_enablement(ctx);
|
||||
});
|
||||
|
||||
ctx.subscribe_to_model(&CodeSettings::handle(ctx), |_, code_settings_event, ctx| {
|
||||
match code_settings_event {
|
||||
ctx.subscribe_to_model(
|
||||
&CodeSettings::handle(ctx),
|
||||
|_, _, code_settings_event, ctx| match code_settings_event {
|
||||
CodeSettingsChangedEvent::CodebaseContextEnabled { .. }
|
||||
| CodeSettingsChangedEvent::AutoIndexingEnabled { .. } => {
|
||||
ctx.emit(UserWorkspacesEvent::CodebaseContextEnablementChanged);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
ctx.subscribe_to_model(&AISettings::handle(ctx), |_, ai_settings_event, ctx| {
|
||||
ctx.subscribe_to_model(&AISettings::handle(ctx), |_, _, ai_settings_event, ctx| {
|
||||
if let AISettingsChangedEvent::IsAnyAIEnabled { .. } = ai_settings_event {
|
||||
ctx.emit(UserWorkspacesEvent::CodebaseContextEnablementChanged);
|
||||
}
|
||||
@@ -387,7 +379,7 @@ impl UserWorkspaces {
|
||||
/// Returns `true` if active AI is allowed for the current workspace, based on billing config.
|
||||
///
|
||||
/// In the future, we should store active AI enablement on the policy directly. For now, we
|
||||
/// proxy whether active AI by checking if prompt suggestions, next command, or code suggestions are enabled.
|
||||
/// proxy whether active AI by checking whether any active AI feature is enabled.
|
||||
pub fn is_active_ai_allowed(&self) -> bool {
|
||||
self.current_team().is_none_or(|team| {
|
||||
team.billing_metadata
|
||||
@@ -397,6 +389,7 @@ impl UserWorkspaces {
|
||||
policy.is_prompt_suggestions_toggleable
|
||||
|| policy.is_next_command_enabled
|
||||
|| policy.is_code_suggestions_toggleable
|
||||
|| policy.is_git_operations_ai_enabled
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -454,6 +447,19 @@ impl UserWorkspaces {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether Git Operations AI is enabled for the current user, based on the active policies.
|
||||
/// Note that the value may be incorrect if called before the team's billing metadata has been fetched.
|
||||
pub fn is_git_operations_ai_enabled(&self) -> bool {
|
||||
self.current_team()
|
||||
// If the user has no team, they can toggle Git Operations AI (no restrictions).
|
||||
.is_none_or(|team| {
|
||||
team.billing_metadata
|
||||
.tier
|
||||
.warp_ai_policy
|
||||
.is_some_and(|policy| policy.is_git_operations_ai_enabled)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether voice input should be toggleable for the current user, based on the active policies.
|
||||
/// Note that the value may be incorrect if called before the team's billing metadata has been fetched.
|
||||
/// If voice input support is not compiled into this build, always returns `false`.
|
||||
@@ -473,11 +479,37 @@ impl UserWorkspaces {
|
||||
/// Whether BYO API key is enabled for the current user, based on the active policies.
|
||||
/// Note that the value may be incorrect if called before the team's billing metadata has been fetched.
|
||||
/// For solo users (no workspace), this is controlled by the `SoloUserByok` feature flag.
|
||||
pub fn is_byo_api_key_enabled(&self) -> bool {
|
||||
/// Anonymous or logged-out users are not allowed to use BYO API keys.
|
||||
pub fn is_byo_api_key_enabled(&self, app: &AppContext) -> bool {
|
||||
if AuthStateProvider::as_ref(app)
|
||||
.get()
|
||||
.is_anonymous_or_logged_out()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.current_workspace()
|
||||
.map(|workspace| workspace.is_byo_api_key_enabled())
|
||||
.unwrap_or(FeatureFlag::SoloUserByok.is_enabled())
|
||||
}
|
||||
/// Whether custom inference endpoints are enabled for the current user.
|
||||
/// Anonymous or logged-out users are not allowed to use custom inference.
|
||||
/// Enterprise workspaces require the enterprise custom inference flag, Warp Plan, or dogfood.
|
||||
pub fn is_custom_inference_enabled(&self, app: &AppContext) -> bool {
|
||||
if AuthStateProvider::as_ref(app)
|
||||
.get()
|
||||
.is_anonymous_or_logged_out()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
self.current_workspace()
|
||||
.map(|workspace| {
|
||||
workspace.billing_metadata.customer_type != CustomerType::Enterprise
|
||||
|| FeatureFlag::CustomInferenceEndpointsEnterprise.is_enabled()
|
||||
|| ChannelState::channel().is_dogfood()
|
||||
})
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn aws_bedrock_host_settings(&self) -> Option<&super::workspace::LlmHostSettings> {
|
||||
self.current_workspace().and_then(|workspace| {
|
||||
@@ -525,6 +557,63 @@ impl UserWorkspaces {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gemini_enterprise_host_settings(&self) -> Option<&super::workspace::LlmHostSettings> {
|
||||
self.current_workspace().and_then(|workspace| {
|
||||
workspace
|
||||
.settings
|
||||
.llm_settings
|
||||
.host_configs
|
||||
.get(&LLMModelHost::GeminiEnterprise)
|
||||
})
|
||||
}
|
||||
|
||||
/// Did the admin enable Gemini Enterprise (GEAP) for the current workspace?
|
||||
pub fn is_gemini_enterprise_available_from_workspace(&self) -> bool {
|
||||
self.current_workspace().is_some_and(|workspace| {
|
||||
workspace.settings.llm_settings.enabled
|
||||
&& self
|
||||
.gemini_enterprise_host_settings()
|
||||
.is_some_and(|settings| settings.enabled)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn gemini_enterprise_host_enablement_setting(&self) -> HostEnablementSetting {
|
||||
self.gemini_enterprise_host_settings()
|
||||
.map(|settings| settings.enablement_setting.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn is_gemini_enterprise_credentials_toggleable(&self) -> bool {
|
||||
matches!(
|
||||
self.gemini_enterprise_host_enablement_setting(),
|
||||
HostEnablementSetting::RespectUserSetting
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether Gemini Enterprise (GEAP) credentials should be minted and attached for the
|
||||
/// current user. Anonymous/logged-out guard from [`Self::is_byo_api_key_enabled`]:
|
||||
/// a GEAP credential mint is rooted in the user's Warp session, so without one
|
||||
/// there is nothing to mint from.
|
||||
pub fn is_gemini_enterprise_credentials_enabled(&self, app: &AppContext) -> bool {
|
||||
if AuthStateProvider::as_ref(app)
|
||||
.get()
|
||||
.is_anonymous_or_logged_out()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// i.e. did the admin toggle on Gemini Enterprise in the admin panel?
|
||||
if !self.is_gemini_enterprise_available_from_workspace() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match self.gemini_enterprise_host_enablement_setting() {
|
||||
HostEnablementSetting::Enforce => true,
|
||||
HostEnablementSetting::RespectUserSetting => *AISettings::as_ref(app)
|
||||
.gemini_enterprise_credentials_enabled
|
||||
.value(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the AI autonomy settings that are enforced by the workspace for all its members.
|
||||
/// If a setting is `None`, the workspace doesn't enforce a particular setting.
|
||||
pub fn ai_autonomy_settings(&self) -> AiAutonomySettings {
|
||||
@@ -1456,6 +1545,11 @@ impl UserWorkspaces {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_host_slug(&self) -> Option<&str> {
|
||||
self.current_team()
|
||||
.and_then(|team| team.organization_settings.default_host_slug.as_deref())
|
||||
}
|
||||
|
||||
/// Returns the team-level agent attribution setting.
|
||||
///
|
||||
/// Use this to decide whether the user's attribution toggle should be locked
|
||||
@@ -1544,6 +1638,7 @@ impl UserWorkspaces {
|
||||
}],
|
||||
billing_metadata: BillingMetadata::default(),
|
||||
bonus_grants_purchased_this_month: Default::default(),
|
||||
billing_cycle_usage: None,
|
||||
has_billing_history: false,
|
||||
settings: workspace_settings,
|
||||
invite_code: None,
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use mockall::Sequence;
|
||||
use settings::{PrivatePreferences, PublicPreferences};
|
||||
use warpui::{AddSingletonModel, App};
|
||||
use galaxyui_extras::user_preferences;
|
||||
|
||||
use super::*;
|
||||
use crate::ai::llms::LLMModelHost;
|
||||
use crate::auth::AuthManager;
|
||||
use crate::cloud_object::model::persistence::CloudModel;
|
||||
@@ -22,14 +30,6 @@ use crate::workspaces::workspace::{
|
||||
Workspace,
|
||||
};
|
||||
|
||||
use galaxyui::{AddSingletonModel, App};
|
||||
use galaxyui_extras::user_preferences;
|
||||
use mockall::Sequence;
|
||||
use settings::{PrivatePreferences, PublicPreferences};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Default)]
|
||||
struct CachedResources {
|
||||
workspaces: Vec<Workspace>,
|
||||
@@ -40,6 +40,22 @@ fn initialize_app(
|
||||
resources: CachedResources,
|
||||
team_client: Arc<dyn TeamClient>,
|
||||
workspace_client: Arc<dyn WorkspaceClient>,
|
||||
) {
|
||||
initialize_app_with_auth(
|
||||
app,
|
||||
resources,
|
||||
team_client,
|
||||
workspace_client,
|
||||
AuthStateProvider::new_for_test(),
|
||||
);
|
||||
}
|
||||
|
||||
fn initialize_app_with_auth(
|
||||
app: &mut App,
|
||||
resources: CachedResources,
|
||||
team_client: Arc<dyn TeamClient>,
|
||||
workspace_client: Arc<dyn WorkspaceClient>,
|
||||
auth_state_provider: AuthStateProvider,
|
||||
) {
|
||||
// Add the necessary singleton models to the App
|
||||
app.add_singleton_model(|_| NetworkStatus::new());
|
||||
@@ -59,7 +75,7 @@ fn initialize_app(
|
||||
app.add_singleton_model(UpdateManager::mock);
|
||||
app.add_singleton_model(PrivacySettings::mock);
|
||||
app.add_singleton_model(|_| ServerApiProvider::new_for_test());
|
||||
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
|
||||
app.add_singleton_model(|_| auth_state_provider);
|
||||
app.add_singleton_model(AuthManager::new_for_test);
|
||||
app.add_singleton_model(AppTelemetryContextProvider::new_context_provider);
|
||||
app.add_singleton_model(|_| {
|
||||
@@ -105,6 +121,7 @@ fn test_loading_all_spaces_after_switching_from_offline() {
|
||||
teams: vec![team.clone()],
|
||||
billing_metadata: Default::default(),
|
||||
bonus_grants_purchased_this_month: Default::default(),
|
||||
billing_cycle_usage: None,
|
||||
has_billing_history: false,
|
||||
settings: Default::default(),
|
||||
invite_code: None,
|
||||
@@ -239,6 +256,7 @@ fn test_aws_bedrock_credentials_default_off_when_admin_respects_user_setting() {
|
||||
LlmHostSettings {
|
||||
enabled: true,
|
||||
enablement_setting: HostEnablementSetting::RespectUserSetting,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
@@ -275,6 +293,7 @@ fn test_aws_bedrock_credentials_respect_user_setting() {
|
||||
LlmHostSettings {
|
||||
enabled: true,
|
||||
enablement_setting: HostEnablementSetting::RespectUserSetting,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let mut team_client = MockTeamClient::new();
|
||||
@@ -328,6 +347,7 @@ fn test_aws_bedrock_credentials_enforced_by_admin() {
|
||||
LlmHostSettings {
|
||||
enabled: true,
|
||||
enablement_setting: HostEnablementSetting::Enforce,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let mut team_client = MockTeamClient::new();
|
||||
@@ -371,6 +391,254 @@ fn test_aws_bedrock_credentials_enforced_by_admin() {
|
||||
})
|
||||
}
|
||||
|
||||
const TEST_GCP_AUDIENCE: &str = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/warp-pool/providers/warp-provider";
|
||||
const TEST_GCP_SA_EMAIL: &str = "warp-geap@test-project.iam.gserviceaccount.com";
|
||||
|
||||
fn workspace_with_gemini_enterprise_host(
|
||||
team: &Team,
|
||||
enabled: bool,
|
||||
enablement_setting: HostEnablementSetting,
|
||||
) -> Workspace {
|
||||
let mut workspace = workspace_for_test(team);
|
||||
workspace.settings.llm_settings.enabled = true;
|
||||
workspace.settings.llm_settings.host_configs.insert(
|
||||
LLMModelHost::GeminiEnterprise,
|
||||
LlmHostSettings {
|
||||
enabled,
|
||||
enablement_setting,
|
||||
gcp_audience: Some(TEST_GCP_AUDIENCE.to_string()),
|
||||
gcp_sa_email: Some(TEST_GCP_SA_EMAIL.to_string()),
|
||||
},
|
||||
);
|
||||
workspace
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gemini_enterprise_credentials_default_off_when_admin_respects_user_setting() {
|
||||
let team = team_for_test();
|
||||
let workspace = workspace_with_gemini_enterprise_host(
|
||||
&team,
|
||||
true,
|
||||
HostEnablementSetting::RespectUserSetting,
|
||||
);
|
||||
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(
|
||||
&mut app,
|
||||
CachedResources {
|
||||
workspaces: vec![workspace],
|
||||
},
|
||||
Arc::new(MockTeamClient::new()),
|
||||
Arc::new(MockWorkspaceClient::new()),
|
||||
);
|
||||
|
||||
app.read(|ctx| {
|
||||
assert!(
|
||||
!UserWorkspaces::as_ref(ctx).is_gemini_enterprise_credentials_enabled(ctx),
|
||||
"respect-user-setting should default the local Gemini Enterprise credentials toggle to off"
|
||||
);
|
||||
assert!(
|
||||
UserWorkspaces::as_ref(ctx).is_gemini_enterprise_credentials_toggleable(),
|
||||
"respect-user-setting should leave the local Gemini Enterprise credentials toggle editable"
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gemini_enterprise_credentials_respect_user_setting_honors_member_toggle() {
|
||||
let team = team_for_test();
|
||||
let workspace = workspace_with_gemini_enterprise_host(
|
||||
&team,
|
||||
true,
|
||||
HostEnablementSetting::RespectUserSetting,
|
||||
);
|
||||
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(
|
||||
&mut app,
|
||||
CachedResources {
|
||||
workspaces: vec![workspace],
|
||||
},
|
||||
Arc::new(MockTeamClient::new()),
|
||||
Arc::new(MockWorkspaceClient::new()),
|
||||
);
|
||||
|
||||
AISettings::handle(&app).update(&mut app, |settings, ctx| {
|
||||
let _ = settings
|
||||
.gemini_enterprise_credentials_enabled
|
||||
.set_value(true, ctx);
|
||||
});
|
||||
|
||||
app.read(|ctx| {
|
||||
assert!(
|
||||
UserWorkspaces::as_ref(ctx).is_gemini_enterprise_credentials_enabled(ctx),
|
||||
"respect-user-setting should honor an opted-in Gemini Enterprise credentials toggle"
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gemini_enterprise_credentials_enforced_by_admin() {
|
||||
let team = team_for_test();
|
||||
let workspace =
|
||||
workspace_with_gemini_enterprise_host(&team, true, HostEnablementSetting::Enforce);
|
||||
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(
|
||||
&mut app,
|
||||
CachedResources {
|
||||
workspaces: vec![workspace],
|
||||
},
|
||||
Arc::new(MockTeamClient::new()),
|
||||
Arc::new(MockWorkspaceClient::new()),
|
||||
);
|
||||
|
||||
AISettings::handle(&app).update(&mut app, |settings, ctx| {
|
||||
let _ = settings
|
||||
.gemini_enterprise_credentials_enabled
|
||||
.set_value(false, ctx);
|
||||
});
|
||||
|
||||
app.read(|ctx| {
|
||||
assert!(
|
||||
UserWorkspaces::as_ref(ctx).is_gemini_enterprise_credentials_enabled(ctx),
|
||||
"enforced Gemini Enterprise host policy should ignore the local credentials toggle"
|
||||
);
|
||||
assert!(
|
||||
!UserWorkspaces::as_ref(ctx).is_gemini_enterprise_credentials_toggleable(),
|
||||
"enforced Gemini Enterprise host policy should disable the local credentials toggle"
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gemini_enterprise_credentials_disabled_when_host_disabled() {
|
||||
let team = team_for_test();
|
||||
let workspace =
|
||||
workspace_with_gemini_enterprise_host(&team, false, HostEnablementSetting::Enforce);
|
||||
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(
|
||||
&mut app,
|
||||
CachedResources {
|
||||
workspaces: vec![workspace],
|
||||
},
|
||||
Arc::new(MockTeamClient::new()),
|
||||
Arc::new(MockWorkspaceClient::new()),
|
||||
);
|
||||
|
||||
app.read(|ctx| {
|
||||
assert!(
|
||||
!UserWorkspaces::as_ref(ctx).is_gemini_enterprise_available_from_workspace(),
|
||||
"a disabled Gemini Enterprise host should not be available from the workspace"
|
||||
);
|
||||
assert!(
|
||||
!UserWorkspaces::as_ref(ctx).is_gemini_enterprise_credentials_enabled(ctx),
|
||||
"a disabled Gemini Enterprise host should gate credentials off even under ENFORCE"
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gemini_enterprise_credentials_disabled_when_host_absent() {
|
||||
let team = team_for_test();
|
||||
// Bedrock-only workspace: proves the GEAP gate reads its own host entry.
|
||||
let mut workspace = workspace_for_test(&team);
|
||||
workspace.settings.llm_settings.enabled = true;
|
||||
workspace.settings.llm_settings.host_configs.insert(
|
||||
LLMModelHost::AwsBedrock,
|
||||
LlmHostSettings {
|
||||
enabled: true,
|
||||
enablement_setting: HostEnablementSetting::Enforce,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(
|
||||
&mut app,
|
||||
CachedResources {
|
||||
workspaces: vec![workspace],
|
||||
},
|
||||
Arc::new(MockTeamClient::new()),
|
||||
Arc::new(MockWorkspaceClient::new()),
|
||||
);
|
||||
|
||||
app.read(|ctx| {
|
||||
assert!(
|
||||
UserWorkspaces::as_ref(ctx)
|
||||
.gemini_enterprise_host_settings()
|
||||
.is_none(),
|
||||
"a workspace without a Gemini Enterprise host entry should expose no settings"
|
||||
);
|
||||
assert!(
|
||||
!UserWorkspaces::as_ref(ctx).is_gemini_enterprise_credentials_enabled(ctx),
|
||||
"a workspace without a Gemini Enterprise host entry should gate credentials off"
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gemini_enterprise_credentials_disabled_when_logged_out() {
|
||||
let team = team_for_test();
|
||||
let workspace =
|
||||
workspace_with_gemini_enterprise_host(&team, true, HostEnablementSetting::Enforce);
|
||||
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app_with_auth(
|
||||
&mut app,
|
||||
CachedResources {
|
||||
workspaces: vec![workspace],
|
||||
},
|
||||
Arc::new(MockTeamClient::new()),
|
||||
Arc::new(MockWorkspaceClient::new()),
|
||||
AuthStateProvider::new_logged_out_for_test(),
|
||||
);
|
||||
|
||||
app.read(|ctx| {
|
||||
assert!(
|
||||
!UserWorkspaces::as_ref(ctx).is_gemini_enterprise_credentials_enabled(ctx),
|
||||
"logged-out users should never mint or attach Gemini Enterprise credentials"
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gemini_enterprise_host_settings_carries_federation_config() {
|
||||
let team = team_for_test();
|
||||
let workspace = workspace_with_gemini_enterprise_host(
|
||||
&team,
|
||||
true,
|
||||
HostEnablementSetting::RespectUserSetting,
|
||||
);
|
||||
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(
|
||||
&mut app,
|
||||
CachedResources {
|
||||
workspaces: vec![workspace],
|
||||
},
|
||||
Arc::new(MockTeamClient::new()),
|
||||
Arc::new(MockWorkspaceClient::new()),
|
||||
);
|
||||
|
||||
app.read(|ctx| {
|
||||
let user_workspaces = UserWorkspaces::as_ref(ctx);
|
||||
let settings = user_workspaces
|
||||
.gemini_enterprise_host_settings()
|
||||
.expect("workspace should expose the Gemini Enterprise host settings");
|
||||
assert_eq!(settings.gcp_audience.as_deref(), Some(TEST_GCP_AUDIENCE));
|
||||
assert_eq!(settings.gcp_sa_email.as_deref(), Some(TEST_GCP_SA_EMAIL));
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn workspace_for_test(team: &Team) -> Workspace {
|
||||
Workspace {
|
||||
uid: "workspace_uid123456789".to_string().into(),
|
||||
@@ -379,6 +647,7 @@ fn workspace_for_test(team: &Team) -> Workspace {
|
||||
teams: vec![team.clone()],
|
||||
billing_metadata: Default::default(),
|
||||
bonus_grants_purchased_this_month: Default::default(),
|
||||
billing_cycle_usage: None,
|
||||
has_billing_history: false,
|
||||
settings: Default::default(),
|
||||
invite_code: None,
|
||||
@@ -552,6 +821,7 @@ fn test_joining_team_moves_objects() {
|
||||
teams: vec![team.clone()],
|
||||
billing_metadata: Default::default(),
|
||||
bonus_grants_purchased_this_month: Default::default(),
|
||||
billing_cycle_usage: None,
|
||||
has_billing_history: false,
|
||||
settings: Default::default(),
|
||||
invite_code: None,
|
||||
@@ -737,6 +1007,7 @@ fn test_leaving_team_moves_objects() {
|
||||
teams: vec![team.clone()],
|
||||
billing_metadata: Default::default(),
|
||||
bonus_grants_purchased_this_month: Default::default(),
|
||||
billing_cycle_usage: None,
|
||||
has_billing_history: false,
|
||||
settings: Default::default(),
|
||||
invite_code: None,
|
||||
|
||||
+164
-19
@@ -1,17 +1,23 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use chrono::Utc;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use warp_graphql::billing::{AddonCreditAutoReloadStatus, ServiceAgreement, ServiceAgreementType};
|
||||
pub use warp_graphql::billing::{
|
||||
AiCreditsUsageAndCostSubjectType, AiCreditsUsageAndCostType, AiCreditsUsageBucket,
|
||||
AiCreditsUsageSource,
|
||||
};
|
||||
|
||||
use super::team::{MembershipRole, Team};
|
||||
use crate::ai::execution_profiles::{
|
||||
ActionPermission, ComputerUsePermission, WriteToPtyPermission,
|
||||
};
|
||||
use crate::ai::llms::LLMModelHost;
|
||||
use crate::{auth::UserUid, server::ids::ServerId, settings::AgentModeCommandExecutionPredicate};
|
||||
use chrono::Utc;
|
||||
use galaxy_graphql::billing::{
|
||||
AddonCreditAutoReloadStatus, ServiceAgreement, ServiceAgreementType,
|
||||
};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Ordering, path::PathBuf};
|
||||
|
||||
use super::team::{MembershipRole, Team};
|
||||
use crate::auth::UserUid;
|
||||
use crate::server::ids::ServerId;
|
||||
use crate::settings::AgentModeCommandExecutionPredicate;
|
||||
|
||||
#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq)]
|
||||
pub struct WorkspaceUid(ServerId);
|
||||
@@ -39,6 +45,7 @@ pub struct Workspace {
|
||||
pub teams: Vec<Team>,
|
||||
pub billing_metadata: BillingMetadata,
|
||||
pub bonus_grants_purchased_this_month: BonusGrantsPurchased,
|
||||
pub billing_cycle_usage: Option<BillingCycleUsageData>,
|
||||
pub has_billing_history: bool,
|
||||
pub settings: WorkspaceSettings,
|
||||
pub invite_code: Option<WorkspaceInviteCode>,
|
||||
@@ -67,6 +74,7 @@ impl Workspace {
|
||||
teams: teams.unwrap_or_default(),
|
||||
billing_metadata,
|
||||
bonus_grants_purchased_this_month: Default::default(),
|
||||
billing_cycle_usage: None,
|
||||
has_billing_history: false,
|
||||
settings: Default::default(), // TODO: persistence wrapper instead of default
|
||||
invite_code: Default::default(),
|
||||
@@ -87,6 +95,20 @@ impl Workspace {
|
||||
.is_some_and(|member| member.role.is_admin_or_owner())
|
||||
}
|
||||
|
||||
pub fn resolve_usage_visibility(&self, is_admin: bool) -> UsageVisibility {
|
||||
let Some(policy) = self.billing_metadata.tier.usage_visibility_policy else {
|
||||
return UsageVisibility::default();
|
||||
};
|
||||
UsageVisibility {
|
||||
granularity: if is_admin {
|
||||
policy.admin_granularity
|
||||
} else {
|
||||
UsageVisibilityGranularity::OwnOnly
|
||||
},
|
||||
max_prior_cycles: policy.max_prior_cycles,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_be_deleted(&self, current_user_email: &str) -> bool {
|
||||
// Current user needs to be an admin and be the only user remaining
|
||||
self.is_workspace_admin(current_user_email)
|
||||
@@ -241,7 +263,7 @@ impl Ord for InviteLinkDomainRestriction {
|
||||
}
|
||||
}
|
||||
|
||||
/// This enum is the rust represenation of `CustomerType` from the GraphQL Schema.
|
||||
/// This enum is the rust representation of `CustomerType` from the GraphQL Schema.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub enum CustomerType {
|
||||
#[default]
|
||||
@@ -294,6 +316,7 @@ pub struct WarpAiPolicy {
|
||||
pub is_code_suggestions_toggleable: bool,
|
||||
pub is_prompt_suggestions_toggleable: bool,
|
||||
pub is_next_command_enabled: bool,
|
||||
pub is_git_operations_ai_enabled: bool,
|
||||
pub is_voice_enabled: bool,
|
||||
}
|
||||
#[derive(Clone, Debug, Copy, Serialize, Deserialize)]
|
||||
@@ -385,6 +408,48 @@ pub struct InstanceShape {
|
||||
pub memory_gb: i32,
|
||||
}
|
||||
|
||||
/// Granularity at which a viewer can see AI usage across their team.
|
||||
/// Non-admins always collapse to `OwnOnly` regardless of tier.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum UsageVisibilityGranularity {
|
||||
#[default]
|
||||
OwnOnly,
|
||||
TeamAggregate,
|
||||
PerUserTotals,
|
||||
FullBreakdown,
|
||||
}
|
||||
|
||||
/// Number of prior billing cycles a viewer can scroll back through, in
|
||||
/// addition to the always-visible current cycle. Plan-wide; applies to
|
||||
/// admins and non-admins alike.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MaxPriorCycles {
|
||||
#[default]
|
||||
None,
|
||||
/// Current cycle plus `n` prior cycles (`n >= 1`).
|
||||
Limited(u32),
|
||||
Unlimited,
|
||||
}
|
||||
|
||||
/// Rust representation of the `UsageVisibilityPolicy` tier policy from the
|
||||
/// GraphQL schema.
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct UsageVisibilityPolicy {
|
||||
pub admin_granularity: UsageVisibilityGranularity,
|
||||
pub max_prior_cycles: MaxPriorCycles,
|
||||
}
|
||||
|
||||
/// Effective per-viewer visibility, after combining the tier's
|
||||
/// `UsageVisibilityPolicy` with the viewer's admin status. Non-admins always
|
||||
/// collapse to `granularity == OwnOnly`; `max_prior_cycles` is plan-wide and
|
||||
/// applies to admins and non-admins alike. Built by
|
||||
/// [`Workspace::resolve_usage_visibility`].
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct UsageVisibility {
|
||||
pub granularity: UsageVisibilityGranularity,
|
||||
pub max_prior_cycles: MaxPriorCycles,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub enum HostEnablementSetting {
|
||||
Enforce,
|
||||
@@ -414,6 +479,7 @@ pub struct Tier {
|
||||
pub enterprise_credits_auto_reload_policy: Option<EnterpriseCreditsAutoReloadPolicy>,
|
||||
pub multi_admin_policy: Option<MultiAdminPolicy>,
|
||||
pub ambient_agents_policy: Option<AmbientAgentsPolicy>,
|
||||
pub usage_visibility_policy: Option<UsageVisibilityPolicy>,
|
||||
}
|
||||
|
||||
/// This struct is the rust representation of `BillingMetadata` from the GraphQL Schema.
|
||||
@@ -442,6 +508,53 @@ pub struct AiOverages {
|
||||
pub current_period_end: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// A single redacted usage entry from `Workspace.billingCycleUsageHistory`.
|
||||
///
|
||||
/// The shape of this entry depends on the viewer's resolved `UsageVisibility`:
|
||||
/// * `OwnOnly` viewers receive only their own entries with real `cost_type` /
|
||||
/// `usage_bucket` / `usage_source` values.
|
||||
/// * `TeamAggregate` viewers receive exactly one synthetic `TEAM` row per cycle
|
||||
/// carrying `Aggregate` sentinels for all three categorical fields.
|
||||
/// * `PerUserTotals` viewers receive one row per user / service account per
|
||||
/// cycle, also with `Aggregate` sentinels on the categorical fields.
|
||||
/// * `FullBreakdown` viewers receive every real row, one per
|
||||
/// `(subject, cost_type, bucket, source)` tuple. Categorical fields always
|
||||
/// carry real values — the server does **not** synthesize an aggregate team
|
||||
/// total at this granularity. Compute team-wide sums client-side if needed.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BillingCycleUsageEntry {
|
||||
pub subject_type: AiCreditsUsageAndCostSubjectType,
|
||||
pub subject_uid: Option<String>,
|
||||
pub subject_display_name: Option<String>,
|
||||
pub cost_type: AiCreditsUsageAndCostType,
|
||||
pub usage_bucket: AiCreditsUsageBucket,
|
||||
pub usage_source: AiCreditsUsageSource,
|
||||
pub credits_used: i32,
|
||||
pub cost_cents: i32,
|
||||
}
|
||||
|
||||
/// Per-cycle bucket of redacted usage entries with explicit period bounds.
|
||||
/// `period_end` is exclusive (e.g. a summary covering May 2026 has
|
||||
/// `period_end = 2026-06-01T00:00:00Z`).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BillingCycleUsageSummary {
|
||||
pub period_start: chrono::DateTime<chrono::Utc>,
|
||||
pub period_end: chrono::DateTime<chrono::Utc>,
|
||||
pub entries: Vec<BillingCycleUsageEntry>,
|
||||
}
|
||||
|
||||
/// The full per-cycle usage history for a workspace, as redacted by the
|
||||
/// server's `USAGE_VISIBILITY` policy. `current_period_start` /
|
||||
/// `current_period_end` mark the cycle that's currently active; older
|
||||
/// summaries cover prior cycles and the number of them retained is governed
|
||||
/// by the policy's `max_prior_cycles`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BillingCycleUsageData {
|
||||
pub current_period_start: chrono::DateTime<chrono::Utc>,
|
||||
pub current_period_end: chrono::DateTime<chrono::Utc>,
|
||||
pub summaries: Vec<BillingCycleUsageSummary>,
|
||||
}
|
||||
|
||||
impl BillingMetadata {
|
||||
/// Returns whether the current tier has a usage-based pricing policy that can be toggled.
|
||||
pub fn is_usage_based_pricing_toggleable(&self) -> bool {
|
||||
@@ -530,6 +643,22 @@ impl BillingMetadata {
|
||||
|
||||
pub fn is_on_build_business_plan(&self) -> bool {
|
||||
self.customer_type == CustomerType::Business
|
||||
&& matches!(
|
||||
self.service_agreements.first().map(|sa| &sa.type_),
|
||||
Some(ServiceAgreementType::SelfServe)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_on_legacy_business_plan(&self) -> bool {
|
||||
self.customer_type == CustomerType::Business && !self.is_on_build_business_plan()
|
||||
}
|
||||
|
||||
pub fn is_enterprise_plan(&self) -> bool {
|
||||
self.customer_type == CustomerType::Enterprise
|
||||
}
|
||||
|
||||
pub fn is_free_plan(&self) -> bool {
|
||||
self.customer_type == CustomerType::Free
|
||||
}
|
||||
|
||||
pub fn is_on_legacy_paid_plan(&self) -> bool {
|
||||
@@ -538,14 +667,7 @@ impl BillingMetadata {
|
||||
| CustomerType::Turbo
|
||||
| CustomerType::Lightspeed
|
||||
| CustomerType::SelfServe => true,
|
||||
CustomerType::Business => {
|
||||
// Legacy Business has a non-SelfServe service agreement type;
|
||||
// Build Business uses SelfServe. See gql_convert.rs for context.
|
||||
!matches!(
|
||||
self.service_agreements.first().map(|sa| &sa.type_),
|
||||
Some(ServiceAgreementType::SelfServe)
|
||||
)
|
||||
}
|
||||
CustomerType::Business => self.is_on_legacy_business_plan(),
|
||||
CustomerType::Free
|
||||
| CustomerType::Legacy
|
||||
| CustomerType::Enterprise
|
||||
@@ -609,12 +731,33 @@ impl BillingMetadata {
|
||||
.enterprise_credits_auto_reload_policy
|
||||
.is_some_and(|policy| policy.enabled)
|
||||
}
|
||||
|
||||
pub fn is_purchase_add_on_credits_policy_enabled(&self) -> bool {
|
||||
self.tier
|
||||
.purchase_add_on_credits_policy
|
||||
.is_some_and(|policy| policy.enabled)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "workspace_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct LlmHostSettings {
|
||||
pub enabled: bool,
|
||||
pub enablement_setting: HostEnablementSetting,
|
||||
/// Full resource name of the GCP workload identity provider that Gemini Enterprise
|
||||
/// (GEAP) credential minting exchanges Warp OIDC JWTs against. Only populated on the
|
||||
/// `GeminiEnterprise` host entry; `None` for other hosts and for workspace caches
|
||||
/// written before this field existed.
|
||||
#[serde(default)]
|
||||
pub gcp_audience: Option<String>,
|
||||
/// Email of the GCP service account that Gemini Enterprise credential minting
|
||||
/// impersonates after the STS exchange. `None` (or empty) means the federated token
|
||||
/// is used directly.
|
||||
#[serde(default)]
|
||||
pub gcp_sa_email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
@@ -780,4 +923,6 @@ pub struct WorkspaceSettings {
|
||||
/// user toggle is locked. When `RespectUserSetting` (or absent), the user can choose.
|
||||
#[serde(default)]
|
||||
pub enable_warp_attribution: AdminEnablementSetting,
|
||||
#[serde(default)]
|
||||
pub default_host_slug: Option<String>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
use super::*;
|
||||
use crate::server::ids::ServerId;
|
||||
|
||||
// `ServerId::from_string_lossy` requires exactly 22 characters.
|
||||
const TEST_WORKSPACE_UID: &str = "workspace_uid123456789";
|
||||
|
||||
fn make_workspace(policy: Option<UsageVisibilityPolicy>) -> Workspace {
|
||||
let mut workspace = Workspace::from_local_cache(
|
||||
ServerId::from_string_lossy(TEST_WORKSPACE_UID).into(),
|
||||
"Test Workspace".to_string(),
|
||||
None,
|
||||
);
|
||||
workspace.billing_metadata.tier.usage_visibility_policy = policy;
|
||||
workspace
|
||||
}
|
||||
|
||||
fn policy(
|
||||
granularity: UsageVisibilityGranularity,
|
||||
max_prior_cycles: MaxPriorCycles,
|
||||
) -> UsageVisibilityPolicy {
|
||||
UsageVisibilityPolicy {
|
||||
admin_granularity: granularity,
|
||||
max_prior_cycles,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_policy_returns_defaults_for_admin_and_non_admin() {
|
||||
let workspace = make_workspace(None);
|
||||
|
||||
let as_admin = workspace.resolve_usage_visibility(true);
|
||||
assert_eq!(as_admin.granularity, UsageVisibilityGranularity::OwnOnly);
|
||||
assert_eq!(as_admin.max_prior_cycles, MaxPriorCycles::None);
|
||||
|
||||
let as_non_admin = workspace.resolve_usage_visibility(false);
|
||||
assert_eq!(
|
||||
as_non_admin.granularity,
|
||||
UsageVisibilityGranularity::OwnOnly
|
||||
);
|
||||
assert_eq!(as_non_admin.max_prior_cycles, MaxPriorCycles::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_admin_collapses_granularity_but_keeps_max_prior_cycles() {
|
||||
let workspace = make_workspace(Some(policy(
|
||||
UsageVisibilityGranularity::FullBreakdown,
|
||||
MaxPriorCycles::Limited(11),
|
||||
)));
|
||||
|
||||
let resolved = workspace.resolve_usage_visibility(false);
|
||||
|
||||
assert_eq!(resolved.granularity, UsageVisibilityGranularity::OwnOnly);
|
||||
assert_eq!(resolved.max_prior_cycles, MaxPriorCycles::Limited(11));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_inherits_tier_team_aggregate_granularity() {
|
||||
let workspace = make_workspace(Some(policy(
|
||||
UsageVisibilityGranularity::TeamAggregate,
|
||||
MaxPriorCycles::Limited(11),
|
||||
)));
|
||||
|
||||
let resolved = workspace.resolve_usage_visibility(true);
|
||||
|
||||
assert_eq!(
|
||||
resolved.granularity,
|
||||
UsageVisibilityGranularity::TeamAggregate
|
||||
);
|
||||
assert_eq!(resolved.max_prior_cycles, MaxPriorCycles::Limited(11));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_inherits_tier_per_user_totals_unlimited() {
|
||||
let workspace = make_workspace(Some(policy(
|
||||
UsageVisibilityGranularity::PerUserTotals,
|
||||
MaxPriorCycles::Unlimited,
|
||||
)));
|
||||
|
||||
let resolved = workspace.resolve_usage_visibility(true);
|
||||
|
||||
assert_eq!(
|
||||
resolved.granularity,
|
||||
UsageVisibilityGranularity::PerUserTotals
|
||||
);
|
||||
assert_eq!(resolved.max_prior_cycles, MaxPriorCycles::Unlimited);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_inherits_tier_full_breakdown_unlimited() {
|
||||
let workspace = make_workspace(Some(policy(
|
||||
UsageVisibilityGranularity::FullBreakdown,
|
||||
MaxPriorCycles::Unlimited,
|
||||
)));
|
||||
|
||||
let resolved = workspace.resolve_usage_visibility(true);
|
||||
|
||||
assert_eq!(
|
||||
resolved.granularity,
|
||||
UsageVisibilityGranularity::FullBreakdown
|
||||
);
|
||||
assert_eq!(resolved.max_prior_cycles, MaxPriorCycles::Unlimited);
|
||||
}
|
||||
Reference in New Issue
Block a user