first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+253 -464
View File
@@ -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"),
}
}
+4 -2
View File
@@ -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 {
+13 -8
View File
@@ -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>,
) {
+24 -34
View File
@@ -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]
+10 -29
View File
@@ -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,
}
}
+143 -48
View File
@@ -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,
+280 -9
View File
@@ -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
View File
@@ -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>,
}
+102
View File
@@ -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);
}