Skip to content

Commit a25c09b

Browse files
committed
Merge branch 'main' into refactor/centralize-env-var-access
2 parents 094603e + f4fe86e commit a25c09b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+992
-977
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ book/
4949
.env*
5050

5151
run-build.sh
52+
.amazonq/

Cargo.lock

Lines changed: 3 additions & 3 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.19.2"
11+
version = "1.19.3"
1212
license = "MIT OR Apache-2.0"
1313

1414
[workspace.dependencies]

crates/chat-cli-ui/src/conduit.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::legacy_ui_util::ThemeSource;
1515
use crate::protocol::{
1616
Event,
1717
LegacyPassThroughOutput,
18+
MetaEvent,
1819
ToolCallRejection,
1920
ToolCallStart,
2021
};
@@ -53,6 +54,7 @@ impl ViewEnd {
5354
pub fn into_legacy_mode(
5455
self,
5556
theme_source: impl ThemeSource,
57+
prompt_ack: Option<std::sync::mpsc::Sender<()>>,
5658
mut stderr: std::io::Stderr,
5759
mut stdout: std::io::Stdout,
5860
) -> Result<(), ConduitError> {
@@ -153,7 +155,17 @@ impl ViewEnd {
153155
Event::ReasoningMessageEnd(_reasoning_message_end) => {},
154156
Event::ReasoningMessageChunk(_reasoning_message_chunk) => {},
155157
Event::ReasoningEnd(_reasoning_end) => {},
156-
Event::MetaEvent(_meta_event) => {},
158+
Event::MetaEvent(MetaEvent { meta_type, payload }) => {
159+
if meta_type.as_str() == "timing" {
160+
if let serde_json::Value::String(s) = payload {
161+
if s.as_str() == "prompt_user" {
162+
if let Some(prompt_ack) = prompt_ack.as_ref() {
163+
_ = prompt_ack.send(());
164+
}
165+
}
166+
}
167+
}
168+
},
157169
Event::ToolCallRejection(tool_call_rejection) => {
158170
let ToolCallRejection { reason, name, .. } = tool_call_rejection;
159171

crates/chat-cli/src/auth/builder_id.rs

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,16 @@ pub enum TokenType {
274274
IamIdentityCenter,
275275
}
276276

277+
impl From<Option<&str>> for TokenType {
278+
fn from(start_url: Option<&str>) -> Self {
279+
match start_url {
280+
Some(url) if url == START_URL => TokenType::BuilderId,
281+
None => TokenType::BuilderId,
282+
Some(_) => TokenType::IamIdentityCenter,
283+
}
284+
}
285+
}
286+
277287
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
278288
pub struct BuilderIdToken {
279289
pub access_token: Secret,
@@ -303,7 +313,10 @@ impl BuilderIdToken {
303313
}
304314

305315
/// Load the token from the keychain, refresh the token if it is expired and return it
306-
pub async fn load(database: &Database) -> Result<Option<Self>, AuthError> {
316+
pub async fn load(
317+
database: &Database,
318+
telemetry: Option<&crate::telemetry::TelemetryThread>,
319+
) -> Result<Option<Self>, AuthError> {
307320
// Can't use #[cfg(test)] without breaking lints, and we don't want to require
308321
// authentication in order to run ChatSession tests. Hence, adding this here with cfg!(test)
309322
if cfg!(test) {
@@ -329,7 +342,7 @@ impl BuilderIdToken {
329342

330343
if token.is_expired() {
331344
trace!("token is expired, refreshing");
332-
token.refresh_token(&client, database, &region).await
345+
token.refresh_token(&client, database, &region, telemetry).await
333346
} else {
334347
trace!(?token, "found a valid token");
335348
Ok(Some(token))
@@ -358,6 +371,7 @@ impl BuilderIdToken {
358371
client: &Client,
359372
database: &Database,
360373
region: &Region,
374+
telemetry: Option<&crate::telemetry::TelemetryThread>,
361375
) -> Result<Option<Self>, AuthError> {
362376
let Some(refresh_token) = &self.refresh_token else {
363377
warn!("no refresh token was found");
@@ -417,6 +431,25 @@ impl BuilderIdToken {
417431
let display_err = DisplayErrorContext(&err);
418432
error!("Failed to refresh builder id access token: {}", display_err);
419433

434+
// Send telemetry for refresh failure
435+
if let Some(telemetry) = telemetry {
436+
let auth_method = match self.token_type() {
437+
TokenType::BuilderId => "BuilderId",
438+
TokenType::IamIdentityCenter => "IdentityCenter",
439+
};
440+
let oauth_flow = match self.oauth_flow {
441+
OAuthFlow::DeviceCode => "DeviceCode",
442+
OAuthFlow::Pkce => "PKCE",
443+
};
444+
let error_code = match &err {
445+
SdkError::ServiceError(service_err) => service_err.err().meta().code().map(|s| s.to_string()),
446+
_ => None,
447+
};
448+
telemetry
449+
.send_auth_failed(auth_method, oauth_flow, "TokenRefresh", error_code)
450+
.ok();
451+
}
452+
420453
// if the error is the client's fault, clear the token
421454
if let SdkError::ServiceError(service_err) = &err {
422455
if !service_err.err().is_slow_down_exception() {
@@ -472,11 +505,7 @@ impl BuilderIdToken {
472505
}
473506

474507
pub fn token_type(&self) -> TokenType {
475-
match &self.start_url {
476-
Some(url) if url == START_URL => TokenType::BuilderId,
477-
None => TokenType::BuilderId,
478-
Some(_) => TokenType::IamIdentityCenter,
479-
}
508+
TokenType::from(self.start_url.as_deref())
480509
}
481510

482511
/// Check if the token is for the internal amzn start URL (`https://amzn.awsapps.com/start`),
@@ -499,6 +528,7 @@ pub async fn poll_create_token(
499528
device_code: String,
500529
start_url: Option<String>,
501530
region: Option<String>,
531+
telemetry: &crate::telemetry::TelemetryThread,
502532
) -> PollCreateToken {
503533
let region = region.clone().map_or(OIDC_BUILDER_ID_REGION, Region::new);
504534
let client = client(region.clone());
@@ -539,6 +569,20 @@ pub async fn poll_create_token(
539569
},
540570
Err(err) => {
541571
error!(?err, "Failed to poll for builder id token");
572+
573+
// Send telemetry for device code failure
574+
let auth_method = match TokenType::from(start_url.as_deref()) {
575+
TokenType::BuilderId => "BuilderId",
576+
TokenType::IamIdentityCenter => "IdentityCenter",
577+
};
578+
let error_code = match &err {
579+
SdkError::ServiceError(service_err) => service_err.err().meta().code().map(|s| s.to_string()),
580+
_ => None,
581+
};
582+
telemetry
583+
.send_auth_failed(auth_method, "DeviceCode", "NewLogin", error_code)
584+
.ok();
585+
542586
PollCreateToken::Error(err.into())
543587
},
544588
}
@@ -551,7 +595,7 @@ pub async fn is_logged_in(database: &mut Database) -> bool {
551595
return true;
552596
}
553597

554-
match BuilderIdToken::load(database).await {
598+
match BuilderIdToken::load(database, None).await {
555599
Ok(Some(_)) => true,
556600
Ok(None) => {
557601
info!("not logged in - no valid token found");
@@ -586,7 +630,7 @@ pub async fn logout(database: &mut Database) -> Result<(), AuthError> {
586630
pub async fn get_start_url_and_region(database: &Database) -> (Option<String>, Option<String>) {
587631
// NOTE: Database provides direct methods to access the start_url and region, but they are not
588632
// guaranteed to be up to date in the chat session. Example: login is changed mid-chat session.
589-
let token = BuilderIdToken::load(database).await;
633+
let token = BuilderIdToken::load(database, None).await;
590634
match token {
591635
Ok(Some(t)) => (t.start_url, t.region),
592636
_ => (None, None),
@@ -604,7 +648,7 @@ impl ResolveIdentity for BearerResolver {
604648
) -> IdentityFuture<'a> {
605649
IdentityFuture::new_boxed(Box::pin(async {
606650
let database = Database::new().await?;
607-
match BuilderIdToken::load(&database).await? {
651+
match BuilderIdToken::load(&database, None).await? {
608652
Some(token) => Ok(Identity::new(
609653
Token::new(token.access_token.0.clone(), Some(token.expires_at.into())),
610654
Some(token.expires_at.into()),
@@ -619,7 +663,7 @@ pub async fn is_idc_user(database: &Database) -> Result<bool> {
619663
if cfg!(test) {
620664
return Ok(false);
621665
}
622-
if let Ok(Some(token)) = BuilderIdToken::load(database).await {
666+
if let Ok(Some(token)) = BuilderIdToken::load(database, None).await {
623667
Ok(token.token_type() == TokenType::IamIdentityCenter)
624668
} else {
625669
Err(eyre!("No auth token found - is the user signed in?"))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub enum AuthError {
3131
#[error(transparent)]
3232
TimeComponentRange(#[from] time::error::ComponentRange),
3333
#[error(transparent)]
34-
Directories(#[from] crate::util::directories::DirectoryError),
34+
Directories(#[from] crate::util::paths::DirectoryError),
3535
#[error(transparent)]
3636
SerdeJson(#[from] serde_json::Error),
3737
#[error(transparent)]

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

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::cli::agent::hook::Hook;
1919
use crate::cli::agent::legacy::context::LegacyContextConfig;
2020
use crate::os::Os;
2121
use crate::theme::StyledText;
22-
use crate::util::directories;
22+
use crate::util::paths::PathResolver;
2323

2424
/// Performs the migration from legacy profile configuration to agent configuration if it hasn't
2525
/// already been done.
@@ -32,15 +32,16 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
3232
return Ok(None);
3333
}
3434

35-
let legacy_global_context_path = directories::chat_global_context_path(os)?;
35+
let resolver = PathResolver::new(os);
36+
let legacy_global_context_path = resolver.global().global_context()?;
3637
let legacy_global_context: Option<LegacyContextConfig> = 'global: {
3738
let Ok(content) = os.fs.read(&legacy_global_context_path).await else {
3839
break 'global None;
3940
};
4041
serde_json::from_slice::<LegacyContextConfig>(&content).ok()
4142
};
4243

43-
let legacy_profile_path = directories::chat_profiles_dir(os)?;
44+
let legacy_profile_path = resolver.global().profiles_dir()?;
4445
let mut legacy_profiles: HashMap<String, LegacyContextConfig> = 'profiles: {
4546
let mut profiles = HashMap::<String, LegacyContextConfig>::new();
4647
let Ok(mut read_dir) = os.fs.read_dir(&legacy_profile_path).await else {
@@ -78,7 +79,7 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
7879
};
7980

8081
let mcp_servers = {
81-
let config_path = directories::chat_legacy_global_mcp_config(os)?;
82+
let config_path = resolver.global().mcp_config()?;
8283
if os.fs.exists(&config_path) {
8384
match McpServerConfig::load_from_file(os, config_path).await {
8485
Ok(mut config) => {
@@ -145,7 +146,12 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
145146
new_agents.push(Agent {
146147
name: LEGACY_GLOBAL_AGENT_NAME.to_string(),
147148
description: Some(DEFAULT_DESC.to_string()),
148-
path: Some(directories::chat_global_agent_path(os)?.join(format!("{LEGACY_GLOBAL_AGENT_NAME}.json"))),
149+
path: Some(
150+
resolver
151+
.global()
152+
.agents_dir()?
153+
.join(format!("{LEGACY_GLOBAL_AGENT_NAME}.json")),
154+
),
149155
resources: context.paths.iter().map(|p| format!("file://{p}").into()).collect(),
150156
hooks: HashMap::from([
151157
(
@@ -168,7 +174,7 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
168174
});
169175
}
170176

171-
let global_agent_path = directories::chat_global_agent_path(os)?;
177+
let global_agent_path = resolver.global().ensure_agents_dir().await?;
172178

173179
// Migration of profile context
174180
for (profile_name, context) in legacy_profiles.drain() {
@@ -205,10 +211,6 @@ pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>
205211
});
206212
}
207213

208-
if !os.fs.exists(&global_agent_path) {
209-
os.fs.create_dir_all(&global_agent_path).await?;
210-
}
211-
212214
for agent in &mut new_agents {
213215
let content = agent.to_str_pretty()?;
214216
if let Some(path) = agent.path.as_ref() {

0 commit comments

Comments
 (0)