Skip to content

Commit 2d9c3b4

Browse files
author
kiran-garre
committed
Merge branch 'main' into kiran-garre/todo-list
2 parents cbc8625 + 2bf1c40 commit 2d9c3b4

File tree

7 files changed

+174
-34
lines changed

7 files changed

+174
-34
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ authors = ["Amazon Q CLI Team ([email protected])", "Chay Nabors (nabochay@amazon
88
edition = "2024"
99
homepage = "https://aws.amazon.com/q/"
1010
publish = false
11-
version = "1.13.3"
11+
version = "1.14.0"
1212
license = "MIT OR Apache-2.0"
1313

1414
[workspace.dependencies]
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use std::time::{
2+
Duration,
3+
Instant,
4+
};
5+
6+
use aws_smithy_runtime_api::box_error::BoxError;
7+
use aws_smithy_runtime_api::client::interceptors::Intercept;
8+
use aws_smithy_runtime_api::client::interceptors::context::BeforeTransmitInterceptorContextRef;
9+
use aws_smithy_runtime_api::client::retries::RequestAttempts;
10+
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
11+
use aws_smithy_types::config_bag::{
12+
ConfigBag,
13+
Storable,
14+
StoreReplace,
15+
};
16+
use crossterm::style::Color;
17+
use crossterm::{
18+
execute,
19+
style,
20+
};
21+
22+
#[derive(Debug, Clone)]
23+
pub struct DelayTrackingInterceptor {
24+
minor_delay_threshold: Duration,
25+
major_delay_threshold: Duration,
26+
}
27+
28+
impl DelayTrackingInterceptor {
29+
pub fn new() -> Self {
30+
Self {
31+
minor_delay_threshold: Duration::from_secs(2),
32+
major_delay_threshold: Duration::from_secs(5),
33+
}
34+
}
35+
36+
fn print_warning(message: String) {
37+
let mut stderr = std::io::stderr();
38+
let _ = execute!(
39+
stderr,
40+
style::SetForegroundColor(Color::Yellow),
41+
style::Print("\nWARNING: "),
42+
style::SetForegroundColor(Color::Reset),
43+
style::Print(message),
44+
style::Print("\n")
45+
);
46+
}
47+
}
48+
49+
impl Intercept for DelayTrackingInterceptor {
50+
fn name(&self) -> &'static str {
51+
"DelayTrackingInterceptor"
52+
}
53+
54+
fn read_before_transmit(
55+
&self,
56+
_: &BeforeTransmitInterceptorContextRef<'_>,
57+
_: &RuntimeComponents,
58+
cfg: &mut ConfigBag,
59+
) -> Result<(), BoxError> {
60+
let attempt_number = cfg.load::<RequestAttempts>().map_or(1, |attempts| attempts.attempts());
61+
62+
let now = Instant::now();
63+
64+
if let Some(last_attempt_time) = cfg.load::<LastAttemptTime>() {
65+
let delay = now.duration_since(last_attempt_time.0);
66+
67+
if delay >= self.major_delay_threshold {
68+
Self::print_warning(format!(
69+
"Auto Retry #{} delayed by {:.1}s. Service is under heavy load - consider switching models.",
70+
attempt_number,
71+
delay.as_secs_f64()
72+
));
73+
} else if delay >= self.minor_delay_threshold {
74+
Self::print_warning(format!(
75+
"Auto Retry #{} delayed by {:.1}s due to transient issues.",
76+
attempt_number,
77+
delay.as_secs_f64()
78+
));
79+
}
80+
}
81+
82+
cfg.interceptor_state().store_put(LastAttemptTime(Instant::now()));
83+
Ok(())
84+
}
85+
}
86+
87+
#[derive(Debug, Clone)]
88+
struct LastAttemptTime(Instant);
89+
90+
impl Storable for LastAttemptTime {
91+
type Storer = StoreReplace<Self>;
92+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod credentials;
22
pub mod customization;
3+
mod delay_interceptor;
34
mod endpoints;
45
mod error;
56
pub mod model;
@@ -41,6 +42,7 @@ use tracing::{
4142
};
4243

4344
use crate::api_client::credentials::CredentialsChain;
45+
use crate::api_client::delay_interceptor::DelayTrackingInterceptor;
4446
use crate::api_client::model::{
4547
ChatResponseStream,
4648
ConversationState,
@@ -163,6 +165,7 @@ impl ApiClient {
163165
.http_client(crate::aws_common::http_client::client())
164166
.interceptor(OptOutInterceptor::new(database))
165167
.interceptor(UserAgentOverrideInterceptor::new())
168+
.interceptor(DelayTrackingInterceptor::new())
166169
.app_name(app_name())
167170
.endpoint_url(endpoint.url())
168171
.retry_classifier(retry_classifier::QCliRetryClassifier::new())
@@ -176,6 +179,7 @@ impl ApiClient {
176179
.http_client(crate::aws_common::http_client::client())
177180
.interceptor(OptOutInterceptor::new(database))
178181
.interceptor(UserAgentOverrideInterceptor::new())
182+
.interceptor(DelayTrackingInterceptor::new())
179183
.bearer_token_resolver(BearerResolver)
180184
.app_name(app_name())
181185
.endpoint_url(endpoint.url())

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

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ pub async fn get_available_models(os: &Os) -> Result<(Vec<ModelInfo>, ModelInfo)
170170
Err(e) => {
171171
tracing::error!("Failed to fetch models from API: {}, using fallback list", e);
172172

173-
let models = get_fallback_models();
173+
let models = get_fallback_models(region);
174174
let default_model = models[0].clone();
175175

176176
Ok((models, default_model))
@@ -188,17 +188,49 @@ fn default_context_window() -> usize {
188188
200_000
189189
}
190190

191-
fn get_fallback_models() -> Vec<ModelInfo> {
192-
vec![
193-
ModelInfo {
194-
model_name: Some("claude-3.7-sonnet".to_string()),
195-
model_id: "claude-3.7-sonnet".to_string(),
196-
context_window_tokens: 200_000,
197-
},
198-
ModelInfo {
199-
model_name: Some("claude-4-sonnet".to_string()),
200-
model_id: "claude-4-sonnet".to_string(),
201-
context_window_tokens: 200_000,
202-
},
203-
]
191+
fn get_fallback_models(region: &str) -> Vec<ModelInfo> {
192+
match region {
193+
"eu-central-1" => vec![
194+
ModelInfo {
195+
model_name: Some("claude-sonnet-4".to_string()),
196+
model_id: "claude-sonnet-4".to_string(),
197+
context_window_tokens: 200_000,
198+
},
199+
ModelInfo {
200+
model_name: Some("claude-3.5-sonnet-v1".to_string()),
201+
model_id: "claude-3.5-sonnet-v1".to_string(),
202+
context_window_tokens: 200_000,
203+
},
204+
],
205+
_ => vec![
206+
ModelInfo {
207+
model_name: Some("claude-sonnet-4".to_string()),
208+
model_id: "claude-sonnet-4".to_string(),
209+
context_window_tokens: 200_000,
210+
},
211+
ModelInfo {
212+
model_name: Some("claude-3.7-sonnet".to_string()),
213+
model_id: "claude-3.7-sonnet".to_string(),
214+
context_window_tokens: 200_000,
215+
},
216+
],
217+
}
218+
}
219+
220+
pub fn normalize_model_name(name: &str) -> &str {
221+
match name {
222+
"claude-4-sonnet" => "claude-sonnet-4",
223+
// can add more mapping for backward compatibility
224+
_ => name,
225+
}
226+
}
227+
228+
pub fn find_model<'a>(models: &'a [ModelInfo], name: &str) -> Option<&'a ModelInfo> {
229+
let normalized = normalize_model_name(name);
230+
models.iter().find(|m| {
231+
m.model_name
232+
.as_deref()
233+
.is_some_and(|n| n.eq_ignore_ascii_case(normalized))
234+
|| m.model_id.eq_ignore_ascii_case(normalized)
235+
})
204236
}

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

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ use crate::auth::builder_id::is_idc_user;
134134
use crate::cli::TodoListState;
135135
use crate::cli::agent::Agents;
136136
use crate::cli::chat::cli::SlashCommand;
137+
use crate::cli::chat::cli::model::find_model;
137138
use crate::cli::chat::cli::prompts::{
138139
GetPromptError,
139140
PromptsSubcommand,
@@ -318,13 +319,7 @@ impl ChatArgs {
318319
// Otherwise, CLI will use a default model when starting chat
319320
let (models, default_model_opt) = get_available_models(os).await?;
320321
let model_id: Option<String> = if let Some(requested) = self.model.as_ref() {
321-
let requested_lower = requested.to_lowercase();
322-
if let Some(m) = models.iter().find(|m| {
323-
m.model_name
324-
.as_deref()
325-
.is_some_and(|n| n.eq_ignore_ascii_case(&requested_lower))
326-
|| m.model_id.eq_ignore_ascii_case(&requested_lower)
327-
}) {
322+
if let Some(m) = find_model(&models, requested) {
328323
Some(m.model_id.clone())
329324
} else {
330325
let available = models
@@ -335,14 +330,9 @@ impl ChatArgs {
335330
bail!("Model '{}' does not exist. Available models: {}", requested, available);
336331
}
337332
} else if let Some(saved) = os.database.settings.get_string(Setting::ChatDefaultModel) {
338-
if let Some(m) = models.iter().find(|m| {
339-
m.model_name.as_deref().is_some_and(|n| n.eq_ignore_ascii_case(&saved))
340-
|| m.model_id.eq_ignore_ascii_case(&saved)
341-
}) {
342-
Some(m.model_id.clone())
343-
} else {
344-
Some(default_model_opt.model_id.clone())
345-
}
333+
find_model(&models, &saved)
334+
.map(|m| m.model_id.clone())
335+
.or(Some(default_model_opt.model_id.clone()))
346336
} else {
347337
Some(default_model_opt.model_id.clone())
348338
};

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ pub struct ExecuteCommand {
4848

4949
impl ExecuteCommand {
5050
pub fn requires_acceptance(&self, allowed_commands: Option<&Vec<String>>, allow_read_only: bool) -> bool {
51+
// Always require acceptance for multi-line commands.
52+
if self.command.contains("\n") || self.command.contains("\r") {
53+
return true;
54+
}
55+
5156
let default_arr = vec![];
5257
let allowed_commands = allowed_commands.unwrap_or(&default_arr);
5358

@@ -64,7 +69,7 @@ impl ExecuteCommand {
6469
let Some(args) = shlex::split(&self.command) else {
6570
return true;
6671
};
67-
const DANGEROUS_PATTERNS: &[&str] = &["<(", "$(", "`", ">", "&&", "||", "&", ";"];
72+
const DANGEROUS_PATTERNS: &[&str] = &["<(", "$(", "`", ">", "&&", "||", "&", ";", "${", "\n", "\r", "IFS"];
6873

6974
if args
7075
.iter()
@@ -106,6 +111,7 @@ impl ExecuteCommand {
106111
arg.contains("-exec") // includes -execdir
107112
|| arg.contains("-delete")
108113
|| arg.contains("-ok") // includes -okdir
114+
|| arg.contains("-fprint") // includes -fprint0 and -fprintf
109115
}) =>
110116
{
111117
return true;
@@ -114,7 +120,11 @@ impl ExecuteCommand {
114120
// Special casing for `grep`. -P flag for perl regexp has RCE issues, apparently
115121
// should not be supported within grep but is flagged as a possibility since this is perl
116122
// regexp.
117-
if cmd == "grep" && cmd_args.iter().any(|arg| arg.contains("-P")) {
123+
if cmd == "grep"
124+
&& cmd_args
125+
.iter()
126+
.any(|arg| arg.contains("-P") || arg.contains("--perl-regexp"))
127+
{
118128
return true;
119129
}
120130
let is_cmd_read_only = READONLY_COMMANDS.contains(&cmd.as_str());
@@ -290,6 +300,14 @@ mod tests {
290300
("cat <<< 'some string here' > myimportantfile", true),
291301
("echo '\n#!/usr/bin/env bash\necho hello\n' > myscript.sh", true),
292302
("cat <<EOF > myimportantfile\nhello world\nEOF", true),
303+
// newline checks
304+
("which ls\ntouch asdf", true),
305+
("which ls\rtouch asdf", true),
306+
// $IFS check
307+
(
308+
r#"IFS=';'; for cmd in "which ls;touch asdf"; do eval "$cmd"; done"#,
309+
true,
310+
),
293311
// Safe piped commands
294312
("find . -name '*.rs' | grep main", false),
295313
("ls -la | grep .git", false),
@@ -307,8 +325,12 @@ mod tests {
307325
true,
308326
),
309327
("find important-dir/ -name '*.txt'", false),
328+
(r#"find / -fprintf "/path/to/file" <data-to-write> -quit"#, true),
329+
(r"find . -${t}exec touch asdf \{\} +", true),
330+
(r"find . -${t:=exec} touch asdf2 \{\} +", true),
310331
// `grep` command arguments
311332
("echo 'test data' | grep -P '(?{system(\"date\")})'", true),
333+
("echo 'test data' | grep --perl-regexp '(?{system(\"date\")})'", true),
312334
];
313335
for (cmd, expected) in cmds {
314336
let tool = serde_json::from_value::<ExecuteCommand>(serde_json::json!({

0 commit comments

Comments
 (0)