Skip to content
Closed
9 changes: 6 additions & 3 deletions app/src/ai/agent_sdk/ambient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ impl AmbientAgentRunner {
vec![]
};

let team_scope = match super::common::resolve_object_scope(&args.scope, ctx) {
let team_scope = match super::common::resolve_environment_team_scope(&args.scope, ctx) {
Ok(team_scope) => team_scope,
Err(err) => {
super::report_fatal_error(err, ctx);
Expand All @@ -404,8 +404,11 @@ impl AmbientAgentRunner {
environment_args.environment = Some(environment_id);
}

let environment_id = match EnvironmentChoice::resolve_for_create(environment_args, ctx)
{
let environment_id = match EnvironmentChoice::resolve_for_create(
environment_args,
&team_scope,
ctx,
) {
Ok(EnvironmentChoice::None) => {
eprintln!("Agent will run without an environment.");
None
Expand Down
19 changes: 15 additions & 4 deletions app/src/ai/agent_sdk/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,12 @@ pub(super) fn resolve_team_scope(
.map_err(|err| describe_team_resolution_error(err, ctx))
}

pub(super) fn resolve_object_scope(
object_scope: &ObjectScope,
pub(super) fn resolve_environment_team_scope(
scope: &ObjectScope,
ctx: &AppContext,
) -> anyhow::Result<TeamScopeForCli> {
UserWorkspaces::as_ref(ctx)
.team_scope_for_cli_object(object_scope)
.team_scope_for_cli_object(scope)
.map_err(|err| describe_team_resolution_error(err, ctx))
}

Expand Down Expand Up @@ -234,6 +234,15 @@ pub(super) fn resolve_owner_for_team_scope(
}
}

pub(super) fn environment_is_visible_to_scope(
environment: &CloudAmbientAgentEnvironment,
team_scope: &(impl TeamScope + ?Sized),
) -> bool {
match environment.permissions().owner {
Owner::User { .. } => true,
Owner::Team { team_uid } => team_scope.team_uid() == Some(team_uid),
}
}
/// Refresh workspace metadata before executing an operation.
///
/// This ensures that team state is up-to-date before creating cloud objects or performing
Expand Down Expand Up @@ -336,10 +345,11 @@ pub enum EnvironmentChoice {
}

impl EnvironmentChoice {
/// Resolve the environment to use when creating an agent integration.
/// Resolve the environment to use when creating an agent operation.
/// Warp Drive *must* have been synced first.
pub fn resolve_for_create(
args: EnvironmentCreateArgs,
team_scope: &(impl TeamScope + ?Sized),
ctx: &AppContext,
) -> Result<Self, ResolveConfigurationError> {
if args.no_environment {
Expand All @@ -351,6 +361,7 @@ impl EnvironmentChoice {
let mut synced_environments: Vec<(ServerId, &CloudAmbientAgentEnvironment)> =
all_environments
.iter()
.filter(|env| environment_is_visible_to_scope(env, team_scope))
.filter_map(|env| {
if let SyncId::ServerId(server_id) = env.sync_id() {
Some((server_id, env))
Expand Down
176 changes: 173 additions & 3 deletions app/src/ai/agent_sdk/common_tests.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
use std::collections::HashMap;
use std::sync::Arc;

use warp_cli::environment::EnvironmentCreateArgs;
use warp_cli::scope::{ObjectScope, TeamSelection};
use warpui::App;

use super::{
classify_agent_mode_base_model_id, parse_ambient_task_id, validate_agent_mode_base_model_id,
EnvironmentChoice, classify_agent_mode_base_model_id, environment_is_visible_to_scope,
parse_ambient_task_id, resolve_environment_team_scope, validate_agent_mode_base_model_id,
validate_agent_mode_base_model_id_for_scope,
};
use crate::LaunchMode;
use crate::ai::cloud_environments::{
AmbientAgentEnvironment, CloudAmbientAgentEnvironment, CloudAmbientAgentEnvironmentModel,
};
use crate::ai::execution_profiles::profiles::AIExecutionProfilesModel;
use crate::ai::llms::{
AvailableLLMs, LLMContextWindow, LLMId, LLMInfo, LLMPreferences, LLMProvider, LLMUsageMetadata,
Expand All @@ -17,20 +23,184 @@ use crate::ai::mcp::TemplatableMCPServerManager;
use crate::auth::AuthStateProvider;
use crate::auth::auth_manager::AuthManager;
use crate::cloud_object::model::persistence::CloudModel;
use crate::cloud_object::{CloudObjectMetadata, CloudObjectPermissions, Owner};
use crate::network::NetworkStatus;
use crate::server::cloud_objects::update_manager::UpdateManager;
use crate::server::ids::ServerId;
use crate::server::ids::{ServerId, SyncId};
use crate::server::server_api::ServerApiProvider;
use crate::server::server_api::team::MockTeamClient;
use crate::server::server_api::workspace::MockWorkspaceClient;
use crate::server::sync_queue::SyncQueue;
use crate::settings::PrivacySettings;
use crate::test_util::settings::initialize_settings_for_tests;
use crate::workspaces::team::{Team, TeamVisibility};
use crate::workspaces::team_tester::TeamTesterStatus;
use crate::workspaces::user_workspaces::{
TeamContextForOperation, TeamlessScopeForTest, UserWorkspaces,
TeamContextForOperation, TeamScope, TeamlessScopeForTest, UserWorkspaces,
};
use crate::workspaces::workspace::{Workspace, WorkspaceUid};
fn environment_with_owner(
sync_id: SyncId,
name: &str,
owner: Owner,
) -> CloudAmbientAgentEnvironment {
let environment = AmbientAgentEnvironment::new(
name.to_string(),
None,
Vec::new(),
"ubuntu:latest".to_string(),
Vec::new(),
);
let mut permissions = CloudObjectPermissions::mock_personal();
permissions.owner = owner;
CloudAmbientAgentEnvironment::new(
sync_id,
CloudAmbientAgentEnvironmentModel::new(environment),
CloudObjectMetadata::mock(),
permissions,
)
}

#[test]
fn environment_scope_includes_personal_and_matching_team_environments() {
let selected_team_uid = ServerId::from(123);
let other_team_uid = ServerId::from(456);
let selected_scope = TeamContextForOperation::new_for_test(selected_team_uid);
let personal_environment = environment_with_owner(
SyncId::ServerId(ServerId::from(1)),
"Personal",
Owner::mock_current_user(),
);
let selected_team_environment = environment_with_owner(
SyncId::ServerId(ServerId::from(2)),
"Selected team",
Owner::Team {
team_uid: selected_team_uid,
},
);
let other_team_environment = environment_with_owner(
SyncId::ServerId(ServerId::from(3)),
"Other team",
Owner::Team {
team_uid: other_team_uid,
},
);

assert!(environment_is_visible_to_scope(
&personal_environment,
&selected_scope
));
assert!(environment_is_visible_to_scope(
&selected_team_environment,
&selected_scope
));
assert!(!environment_is_visible_to_scope(
&other_team_environment,
&selected_scope
));
}

#[test]
fn multi_team_personal_scope_includes_only_personal_environments() {
App::test((), |mut app| async move {
initialize_settings_for_tests(&mut app);
app.add_singleton_model(PrivacySettings::mock);
let user_workspaces = app.add_singleton_model(UserWorkspaces::default_mock);
user_workspaces.update(&mut app, |user_workspaces, ctx| {
user_workspaces.setup_test_workspace(ctx);
user_workspaces.update_current_workspace(
|workspace| {
let mut second_team = workspace.teams[0].clone();
second_team.uid = ServerId::from(456);
second_team.name = "Second team".to_string();
workspace.teams.push(second_team);
},
ctx,
);
});
let implicit_scope = app.read(|ctx| {
resolve_environment_team_scope(
&ObjectScope {
team_selection: TeamSelection { team: None },
personal: false,
},
ctx,
)
});
let personal_scope = app
.read(|ctx| {
resolve_environment_team_scope(
&ObjectScope {
team_selection: TeamSelection { team: None },
personal: true,
},
ctx,
)
})
.expect("explicit personal scope should not require a sole team");
let personal_environment = environment_with_owner(
SyncId::ServerId(ServerId::from(1)),
"Personal",
Owner::mock_current_user(),
);
let team_environment = environment_with_owner(
SyncId::ServerId(ServerId::from(2)),
"Team",
Owner::Team {
team_uid: ServerId::from(123),
},
);

assert!(implicit_scope.is_err());
assert_eq!(personal_scope.team_uid(), None);
assert!(environment_is_visible_to_scope(
&personal_environment,
&personal_scope
));
assert!(!environment_is_visible_to_scope(
&team_environment,
&personal_scope
));
});
}

#[test]
fn explicit_environment_id_remains_resource_authoritative() {
App::test((), |mut app| async move {
let cloud_model = app.add_singleton_model(CloudModel::mock);
let server_id = ServerId::from(123);
let sync_id = SyncId::ServerId(server_id);
let team_environment = environment_with_owner(
sync_id,
"Other team",
Owner::Team {
team_uid: ServerId::from(456),
},
);
cloud_model.update(&mut app, |model, ctx| {
model.create_object(sync_id, team_environment, ctx);
});

let choice = app.update(|ctx| {
EnvironmentChoice::resolve_for_create(
EnvironmentCreateArgs {
environment: Some(server_id.to_string()),
no_environment: false,
},
&TeamlessScopeForTest,
ctx,
)
});

assert_eq!(
choice.unwrap(),
EnvironmentChoice::Environment {
id: server_id.to_string(),
name: "Other team".to_string(),
}
);
});
}

#[test]
fn parse_ambient_task_id_accepts_valid_ids() {
Expand Down
41 changes: 28 additions & 13 deletions app/src/ai/agent_sdk/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ use std::collections::HashSet;

use comfy_table::Cell;
use cynic::QueryBuilder;
use futures::future;
use inquire::error::InquireError;
use inquire::{Confirm, Select};
use serde::Serialize;
use warp_cli::GlobalOptions;
use warp_cli::agent::OutputFormat;
use warp_cli::environment::{EnvironmentCommand, ImageCommand};
use warp_cli::scope::ObjectScope;
use warp_cli::scope::{ObjectScope, TeamSelection};
use warp_graphql::queries::get_oauth_connect_tx_status::OauthConnectTxStatus;
use warp_graphql::queries::list_warp_dev_images::{
ListWarpDevImages, ListWarpDevImagesResult, ListWarpDevImagesVariables,
Expand Down Expand Up @@ -63,8 +64,10 @@ pub fn run(
) -> anyhow::Result<()> {
let runner = ctx.add_singleton_model(|_ctx| EnvironmentCommandRunner);
match command {
EnvironmentCommand::List => {
runner.update(ctx, |runner, ctx| runner.list(global_options, ctx));
EnvironmentCommand::List { team_selection } => {
runner.update(ctx, |runner, ctx| {
runner.list(global_options, team_selection, ctx)
});
Ok(())
}
EnvironmentCommand::Create {
Expand Down Expand Up @@ -184,24 +187,36 @@ impl EnvironmentCommandRunner {
});
}

fn list(&self, global_options: GlobalOptions, ctx: &mut ModelContext<Self>) {
let initial_sync = UpdateManager::as_ref(ctx)
.initial_load_complete()
.with_timeout(WARP_DRIVE_SYNC_TIMEOUT);
fn list(
&self,
global_options: GlobalOptions,
team_selection: TeamSelection,
ctx: &mut ModelContext<Self>,
) {
let refresh_future = super::common::refresh_workspace_metadata(ctx);
let warp_drive_sync_future = super::common::refresh_warp_drive(ctx);
let setup_future = future::try_join(refresh_future, warp_drive_sync_future);

ctx.spawn(initial_sync, move |_, result, ctx| {
if result.is_err() {
super::report_fatal_error(
anyhow::anyhow!("Timed out waiting for Warp Drive to sync"),
ctx,
);
ctx.spawn(setup_future, move |_, result, ctx| {
if let Err(err) = result {
super::report_fatal_error(err, ctx);
return;
}
let team_scope = match super::common::resolve_team_scope(&team_selection, ctx) {
Ok(team_scope) => team_scope,
Err(err) => {
super::report_fatal_error(err, ctx);
return;
}
};

let environments = CloudAmbientAgentEnvironment::get_all(ctx);

let environment_infos: Vec<_> = environments
.iter()
.filter(|environment| {
super::common::environment_is_visible_to_scope(environment, &team_scope)
})
.map(|environment| {
let name = environment.model().string_model.name.clone();
let description = environment.model().string_model.description.clone();
Expand Down
Loading
Loading