Skip to content

Commit e670124

Browse files
committed
feat: add CloudTrail tracking to execute_bash tool
- Add CloudTrail user agent metadata to execute_bash tool for tracking - Consolidate CloudTrail constants in consts.rs to eliminate duplication - Create shared env_vars_with_user_agent() function used by both use_aws and execute_bash - Add comprehensive tests for CloudTrail tracking functionality - Support both Unix and Windows platforms with proper environment variable handling - Maintain backward compatibility and thread-safe implementation
1 parent 6e18c15 commit e670124

File tree

6 files changed

+124
-40
lines changed

6 files changed

+124
-40
lines changed

crates/chat-cli/src/cli/chat/consts.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,9 @@ pub const AGENT_FORMAT_TOOLS_DOC_URL: &str =
2626

2727
pub const AGENT_MIGRATION_DOC_URL: &str =
2828
"https://github.com/aws/amazon-q-developer-cli/blob/main/docs/legacy-profile-to-agent-migration.md";
29+
30+
// The environment variable name where we set additional metadata for the AWS CLI user agent.
31+
pub const USER_AGENT_ENV_VAR: &str = "AWS_EXECUTION_ENV";
32+
pub const USER_AGENT_APP_NAME: &str = "AmazonQ-For-CLI";
33+
pub const USER_AGENT_VERSION_KEY: &str = "Version";
34+
pub const USER_AGENT_VERSION_VALUE: &str = env!("CARGO_PKG_VERSION");

crates/chat-cli/src/cli/chat/tools/execute/mod.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use regex::Regex;
1010
use serde::Deserialize;
1111
use tracing::error;
1212

13+
use super::env_vars_with_user_agent;
1314
use crate::cli::agent::{
1415
Agent,
1516
PermissionEvalResult,
@@ -128,8 +129,8 @@ impl ExecuteCommand {
128129
false
129130
}
130131

131-
pub async fn invoke(&self, output: &mut impl Write) -> Result<InvokeOutput> {
132-
let output = run_command(&self.command, MAX_TOOL_RESPONSE_SIZE / 3, Some(output)).await?;
132+
pub async fn invoke(&self, os: &Os, output: &mut impl Write) -> Result<InvokeOutput> {
133+
let output = run_command(os, &self.command, MAX_TOOL_RESPONSE_SIZE / 3, Some(output)).await?;
133134
let clean_stdout = sanitize_unicode_tags(&output.stdout);
134135
let clean_stderr = sanitize_unicode_tags(&output.stderr);
135136

@@ -437,4 +438,53 @@ mod tests {
437438
let res = tool.eval_perm(&agent);
438439
assert!(matches!(res, PermissionEvalResult::Allow));
439440
}
441+
442+
#[tokio::test]
443+
async fn test_cloudtrail_tracking() {
444+
use crate::cli::chat::consts::{
445+
USER_AGENT_APP_NAME,
446+
USER_AGENT_ENV_VAR,
447+
USER_AGENT_VERSION_KEY,
448+
USER_AGENT_VERSION_VALUE,
449+
};
450+
451+
let os = Os::new().await.unwrap();
452+
453+
// Test that env_vars_with_user_agent sets the AWS_EXECUTION_ENV variable correctly
454+
let env_vars = env_vars_with_user_agent(&os);
455+
456+
// Check that AWS_EXECUTION_ENV is set
457+
assert!(env_vars.contains_key(USER_AGENT_ENV_VAR));
458+
459+
let user_agent_value = env_vars.get(USER_AGENT_ENV_VAR).unwrap();
460+
461+
// Check the format is correct
462+
let expected_metadata = format!(
463+
"{} {}/{}",
464+
USER_AGENT_APP_NAME, USER_AGENT_VERSION_KEY, USER_AGENT_VERSION_VALUE
465+
);
466+
assert!(user_agent_value.contains(&expected_metadata));
467+
}
468+
469+
#[tokio::test]
470+
async fn test_cloudtrail_tracking_with_existing_env() {
471+
use crate::cli::chat::consts::{
472+
USER_AGENT_APP_NAME,
473+
USER_AGENT_ENV_VAR,
474+
};
475+
476+
let os = Os::new().await.unwrap();
477+
478+
// Set an existing AWS_EXECUTION_ENV value (safe because Os uses in-memory hashmap in tests)
479+
unsafe {
480+
os.env.set_var(USER_AGENT_ENV_VAR, "ExistingValue");
481+
}
482+
483+
let env_vars = env_vars_with_user_agent(&os);
484+
let user_agent_value = env_vars.get(USER_AGENT_ENV_VAR).unwrap();
485+
486+
// Should contain both the existing value and our metadata
487+
assert!(user_agent_value.contains("ExistingValue"));
488+
assert!(user_agent_value.contains(USER_AGENT_APP_NAME));
489+
}
440490
}

crates/chat-cli/src/cli/chat/tools/execute/unix.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ use tracing::error;
1212

1313
use super::{
1414
CommandResult,
15+
env_vars_with_user_agent,
1516
format_output,
1617
};
18+
use crate::os::Os;
1719

1820
/// Run a bash command on Unix systems.
1921
/// # Arguments
@@ -23,16 +25,21 @@ use super::{
2325
/// # Returns
2426
/// A [`CommandResult`]
2527
pub async fn run_command<W: Write>(
28+
os: &Os,
2629
command: &str,
2730
max_result_size: usize,
2831
mut updates: Option<W>,
2932
) -> Result<CommandResult> {
3033
let shell = std::env::var("AMAZON_Q_CHAT_SHELL").unwrap_or("bash".to_string());
3134

35+
// Set up environment variables with user agent metadata for CloudTrail tracking
36+
let env_vars = env_vars_with_user_agent(os);
37+
3238
// We need to maintain a handle on stderr and stdout, but pipe it to the terminal as well
3339
let mut child = tokio::process::Command::new(shell)
3440
.arg("-c")
3541
.arg(command)
42+
.envs(env_vars)
3643
.stdin(Stdio::inherit())
3744
.stdout(Stdio::piped())
3845
.stderr(Stdio::piped())
@@ -122,10 +129,12 @@ pub async fn run_command<W: Write>(
122129
mod tests {
123130
use crate::cli::chat::tools::OutputKind;
124131
use crate::cli::chat::tools::execute::ExecuteCommand;
132+
use crate::os::Os;
125133

126134
#[ignore = "todo: fix failing on musl for some reason"]
127135
#[tokio::test]
128136
async fn test_execute_bash_tool() {
137+
let os = Os::new().await.unwrap();
129138
let mut stdout = std::io::stdout();
130139

131140
// Verifying stdout
@@ -134,7 +143,7 @@ mod tests {
134143
});
135144
let out = serde_json::from_value::<ExecuteCommand>(v)
136145
.unwrap()
137-
.invoke(&mut stdout)
146+
.invoke(&os, &mut stdout)
138147
.await
139148
.unwrap();
140149

@@ -152,7 +161,7 @@ mod tests {
152161
});
153162
let out = serde_json::from_value::<ExecuteCommand>(v)
154163
.unwrap()
155-
.invoke(&mut stdout)
164+
.invoke(&os, &mut stdout)
156165
.await
157166
.unwrap();
158167

@@ -170,7 +179,7 @@ mod tests {
170179
});
171180
let out = serde_json::from_value::<ExecuteCommand>(v)
172181
.unwrap()
173-
.invoke(&mut stdout)
182+
.invoke(&os, &mut stdout)
174183
.await
175184
.unwrap();
176185
if let OutputKind::Json(json) = out.output {

crates/chat-cli/src/cli/chat/tools/execute/windows.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ use tracing::error;
1212

1313
use super::{
1414
CommandResult,
15+
env_vars_with_user_agent,
1516
format_output,
1617
};
18+
use crate::os::Os;
1719

1820
/// Run a command on Windows using cmd.exe.
1921
/// # Arguments
@@ -23,14 +25,19 @@ use super::{
2325
/// # Returns
2426
/// A [`CommandResult`]
2527
pub async fn run_command<W: Write>(
28+
os: &Os,
2629
command: &str,
2730
max_result_size: usize,
2831
mut updates: Option<W>,
2932
) -> Result<CommandResult> {
33+
// Set up environment variables with user agent metadata for CloudTrail tracking
34+
let env_vars = env_vars_with_user_agent(os);
35+
3036
// We need to maintain a handle on stderr and stdout, but pipe it to the terminal as well
3137
let mut child = tokio::process::Command::new("cmd")
3238
.arg("/C")
3339
.arg(command)
40+
.envs(env_vars)
3441
.stdin(Stdio::inherit())
3542
.stdout(Stdio::piped())
3643
.stderr(Stdio::piped())
@@ -116,9 +123,11 @@ pub async fn run_command<W: Write>(
116123
mod tests {
117124
use crate::cli::chat::tools::OutputKind;
118125
use crate::cli::chat::tools::execute::ExecuteCommand;
126+
use crate::os::Os;
119127

120128
#[tokio::test]
121129
async fn test_execute_cmd_tool() {
130+
let os = Os::new().await.unwrap();
122131
let mut stdout = std::io::stdout();
123132

124133
// Verifying stdout
@@ -127,7 +136,7 @@ mod tests {
127136
});
128137
let out = serde_json::from_value::<ExecuteCommand>(v)
129138
.unwrap()
130-
.invoke(&mut stdout)
139+
.invoke(&os, &mut stdout)
131140
.await
132141
.unwrap();
133142

@@ -145,7 +154,7 @@ mod tests {
145154
});
146155
let out = serde_json::from_value::<ExecuteCommand>(v)
147156
.unwrap()
148-
.invoke(&mut stdout)
157+
.invoke(&os, &mut stdout)
149158
.await
150159
.unwrap();
151160

@@ -163,7 +172,7 @@ mod tests {
163172
});
164173
let out = serde_json::from_value::<ExecuteCommand>(v)
165174
.unwrap()
166-
.invoke(&mut stdout)
175+
.invoke(&os, &mut stdout)
167176
.await
168177
.unwrap();
169178
if let OutputKind::Json(json) = out.output {

crates/chat-cli/src/cli/chat/tools/mod.rs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,13 @@ use thinking::Thinking;
3838
use tracing::error;
3939
use use_aws::UseAws;
4040

41-
use super::consts::MAX_TOOL_RESPONSE_SIZE;
41+
use super::consts::{
42+
MAX_TOOL_RESPONSE_SIZE,
43+
USER_AGENT_APP_NAME,
44+
USER_AGENT_ENV_VAR,
45+
USER_AGENT_VERSION_KEY,
46+
USER_AGENT_VERSION_VALUE,
47+
};
4248
use super::util::images::RichImageBlocks;
4349
use crate::cli::agent::{
4450
Agent,
@@ -118,7 +124,7 @@ impl Tool {
118124
match self {
119125
Tool::FsRead(fs_read) => fs_read.invoke(os, stdout).await,
120126
Tool::FsWrite(fs_write) => fs_write.invoke(os, stdout, line_tracker).await,
121-
Tool::ExecuteCommand(execute_command) => execute_command.invoke(stdout).await,
127+
Tool::ExecuteCommand(execute_command) => execute_command.invoke(os, stdout).await,
122128
Tool::UseAws(use_aws) => use_aws.invoke(os, stdout).await,
123129
Tool::Custom(custom_tool) => custom_tool.invoke(os, stdout).await,
124130
Tool::GhIssue(gh_issue) => gh_issue.invoke(os, stdout).await,
@@ -425,6 +431,36 @@ pub fn queue_function_result(result: &str, updates: &mut impl Write, is_error: b
425431
Ok(())
426432
}
427433

434+
/// Helper function to set up environment variables with user agent metadata for CloudTrail tracking
435+
pub fn env_vars_with_user_agent(os: &Os) -> std::collections::HashMap<String, String> {
436+
let mut env_vars: std::collections::HashMap<String, String> = std::env::vars().collect();
437+
438+
// Set up additional metadata for the AWS CLI user agent
439+
let user_agent_metadata_value = format!(
440+
"{} {}/{}",
441+
USER_AGENT_APP_NAME, USER_AGENT_VERSION_KEY, USER_AGENT_VERSION_VALUE
442+
);
443+
444+
// Check if the user agent metadata env var already exists using Os
445+
let existing_value = os.env.get(USER_AGENT_ENV_VAR).ok();
446+
447+
// If the user agent metadata env var already exists, append to it, otherwise set it
448+
if let Some(existing_value) = existing_value {
449+
if !existing_value.is_empty() {
450+
env_vars.insert(
451+
USER_AGENT_ENV_VAR.to_string(),
452+
format!("{} {}", existing_value, user_agent_metadata_value),
453+
);
454+
} else {
455+
env_vars.insert(USER_AGENT_ENV_VAR.to_string(), user_agent_metadata_value);
456+
}
457+
} else {
458+
env_vars.insert(USER_AGENT_ENV_VAR.to_string(), user_agent_metadata_value);
459+
}
460+
461+
env_vars
462+
}
463+
428464
#[cfg(test)]
429465
mod tests {
430466
use std::path::MAIN_SEPARATOR;

crates/chat-cli/src/cli/chat/tools/use_aws.rs

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use super::{
2222
InvokeOutput,
2323
MAX_TOOL_RESPONSE_SIZE,
2424
OutputKind,
25+
env_vars_with_user_agent,
2526
};
2627
use crate::cli::agent::{
2728
Agent,
@@ -31,12 +32,6 @@ use crate::os::Os;
3132

3233
const READONLY_OPS: [&str; 6] = ["get", "describe", "list", "ls", "search", "batch_get"];
3334

34-
/// The environment variable name where we set additional metadata for the AWS CLI user agent.
35-
const USER_AGENT_ENV_VAR: &str = "AWS_EXECUTION_ENV";
36-
const USER_AGENT_APP_NAME: &str = "AmazonQ-For-CLI";
37-
const USER_AGENT_VERSION_KEY: &str = "Version";
38-
const USER_AGENT_VERSION_VALUE: &str = env!("CARGO_PKG_VERSION");
39-
4035
// TODO: we should perhaps composite this struct with an interface that we can use to mock the
4136
// actual cli with. That will allow us to more thoroughly test it.
4237
#[derive(Debug, Clone, Deserialize)]
@@ -54,32 +49,11 @@ impl UseAws {
5449
!READONLY_OPS.iter().any(|op| self.operation_name.starts_with(op))
5550
}
5651

57-
pub async fn invoke(&self, _os: &Os, _updates: impl Write) -> Result<InvokeOutput> {
52+
pub async fn invoke(&self, os: &Os, _updates: impl Write) -> Result<InvokeOutput> {
5853
let mut command = tokio::process::Command::new("aws");
59-
command.envs(std::env::vars());
60-
61-
// Set up environment variables
62-
let mut env_vars: std::collections::HashMap<String, String> = std::env::vars().collect();
6354

64-
// Set up additional metadata for the AWS CLI user agent
65-
let user_agent_metadata_value = format!(
66-
"{} {}/{}",
67-
USER_AGENT_APP_NAME, USER_AGENT_VERSION_KEY, USER_AGENT_VERSION_VALUE
68-
);
69-
70-
// If the user agent metadata env var already exists, append to it, otherwise set it
71-
if let Some(existing_value) = env_vars.get(USER_AGENT_ENV_VAR) {
72-
if !existing_value.is_empty() {
73-
env_vars.insert(
74-
USER_AGENT_ENV_VAR.to_string(),
75-
format!("{} {}", existing_value, user_agent_metadata_value),
76-
);
77-
} else {
78-
env_vars.insert(USER_AGENT_ENV_VAR.to_string(), user_agent_metadata_value);
79-
}
80-
} else {
81-
env_vars.insert(USER_AGENT_ENV_VAR.to_string(), user_agent_metadata_value);
82-
}
55+
// Set up environment variables with user agent metadata for CloudTrail tracking
56+
let env_vars = env_vars_with_user_agent(os);
8357

8458
command.envs(env_vars).arg("--region").arg(&self.region);
8559
if let Some(profile_name) = self.profile_name.as_deref() {

0 commit comments

Comments
 (0)