Skip to content

Commit 163ed21

Browse files
committed
Merge branch 'main' into snapshot_revise
2 parents 3601482 + 0c23526 commit 163ed21

Some content is hidden

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

41 files changed

+3053
-257
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 (q-cli@amazon.com)", "Chay Nabors (nabochay@amazon
88
edition = "2024"
99
homepage = "https://aws.amazon.com/q/"
1010
publish = false
11-
version = "1.16.0"
11+
version = "1.16.2"
1212
license = "MIT OR Apache-2.0"
1313

1414
[workspace.dependencies]

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ pub use builder_id::{
1414
pub use consts::START_URL;
1515
use thiserror::Error;
1616

17+
use crate::aws_common::SdkErrorDisplay;
18+
1719
#[derive(Debug, Error)]
1820
pub enum AuthError {
1921
#[error(transparent)]
2022
Ssooidc(Box<aws_sdk_ssooidc::Error>),
21-
#[error(transparent)]
23+
#[error("{}", SdkErrorDisplay(.0))]
2224
SdkRegisterClient(Box<SdkError<RegisterClientError>>),
23-
#[error(transparent)]
25+
#[error("{}", SdkErrorDisplay(.0))]
2426
SdkCreateToken(Box<SdkError<CreateTokenError>>),
25-
#[error(transparent)]
27+
#[error("{}", SdkErrorDisplay(.0))]
2628
SdkStartDeviceAuthorization(Box<SdkError<StartDeviceAuthorizationError>>),
2729
#[error(transparent)]
2830
Io(#[from] std::io::Error),

crates/chat-cli/src/cli/agent/hook.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use std::collections::HashMap;
21
use std::fmt::Display;
32

43
use schemars::JsonSchema;
@@ -11,23 +10,26 @@ const DEFAULT_TIMEOUT_MS: u64 = 30_000;
1110
const DEFAULT_MAX_OUTPUT_SIZE: usize = 1024 * 10;
1211
const DEFAULT_CACHE_TTL_SECONDS: u64 = 0;
1312

14-
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
15-
pub struct Hooks(HashMap<HookTrigger, Hook>);
16-
1713
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, JsonSchema, Hash)]
1814
#[serde(rename_all = "camelCase")]
1915
pub enum HookTrigger {
2016
/// Triggered during agent spawn
2117
AgentSpawn,
2218
/// Triggered per user message submission
2319
UserPromptSubmit,
20+
/// Triggered before tool execution
21+
PreToolUse,
22+
/// Triggered after tool execution
23+
PostToolUse,
2424
}
2525

2626
impl Display for HookTrigger {
2727
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2828
match self {
2929
HookTrigger::AgentSpawn => write!(f, "agentSpawn"),
3030
HookTrigger::UserPromptSubmit => write!(f, "userPromptSubmit"),
31+
HookTrigger::PreToolUse => write!(f, "preToolUse"),
32+
HookTrigger::PostToolUse => write!(f, "postToolUse"),
3133
}
3234
}
3335
}
@@ -61,6 +63,11 @@ pub struct Hook {
6163
#[serde(default = "Hook::default_cache_ttl_seconds")]
6264
pub cache_ttl_seconds: u64,
6365

66+
/// Optional glob matcher for hook
67+
/// Currently used for matching tool name of PreToolUse and PostToolUse hook
68+
#[serde(skip_serializing_if = "Option::is_none")]
69+
pub matcher: Option<String>,
70+
6471
#[schemars(skip)]
6572
#[serde(default, skip_serializing)]
6673
pub source: Source,
@@ -73,6 +80,7 @@ impl Hook {
7380
timeout_ms: Self::default_timeout_ms(),
7481
max_output_size: Self::default_max_output_size(),
7582
cache_ttl_seconds: Self::default_cache_ttl_seconds(),
83+
matcher: None,
7684
source,
7785
}
7886
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ impl From<LegacyHook> for Option<Hook> {
8080
timeout_ms: value.timeout_ms,
8181
max_output_size: value.max_output_size,
8282
cache_ttl_seconds: value.cache_ttl_seconds,
83+
matcher: None,
8384
source: Default::default(),
8485
})
8586
}

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

Lines changed: 82 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -776,32 +776,14 @@ impl Agents {
776776

777777
/// Returns a label to describe the permission status for a given tool.
778778
pub fn display_label(&self, tool_name: &str, origin: &ToolOrigin) -> String {
779-
use crate::util::pattern_matching::matches_any_pattern;
779+
use crate::util::tool_permission_checker::is_tool_in_allowlist;
780780

781781
let tool_trusted = self.get_active().is_some_and(|a| {
782-
if matches!(origin, &ToolOrigin::Native) {
783-
return matches_any_pattern(&a.allowed_tools, tool_name);
784-
}
785-
786-
a.allowed_tools.iter().any(|name| {
787-
name.strip_prefix("@").is_some_and(|remainder| {
788-
remainder
789-
.split_once(MCP_SERVER_TOOL_DELIMITER)
790-
.is_some_and(|(_left, right)| right == tool_name)
791-
|| remainder == <ToolOrigin as Borrow<str>>::borrow(origin)
792-
}) || {
793-
if let Some(server_name) = name.strip_prefix("@").and_then(|s| s.split('/').next()) {
794-
if server_name == <ToolOrigin as Borrow<str>>::borrow(origin) {
795-
let tool_pattern = format!("@{}/{}", server_name, tool_name);
796-
matches_any_pattern(&a.allowed_tools, &tool_pattern)
797-
} else {
798-
false
799-
}
800-
} else {
801-
false
802-
}
803-
}
804-
})
782+
let server_name = match origin {
783+
ToolOrigin::Native => None,
784+
ToolOrigin::McpServer(_) => Some(<ToolOrigin as Borrow<str>>::borrow(origin)),
785+
};
786+
is_tool_in_allowlist(&a.allowed_tools, tool_name, server_name)
805787
});
806788

807789
if tool_trusted || self.trust_all_tools {
@@ -818,9 +800,9 @@ impl Agents {
818800
"fs_read" => "trust working directory".dark_grey(),
819801
"fs_write" => "not trusted".dark_grey(),
820802
#[cfg(not(windows))]
821-
"execute_bash" => "trust read-only commands".dark_grey(),
803+
"execute_bash" => "not trusted".dark_grey(),
822804
#[cfg(windows)]
823-
"execute_cmd" => "trust read-only commands".dark_grey(),
805+
"execute_cmd" => "not trusted".dark_grey(),
824806
"use_aws" => "trust read-only commands".dark_grey(),
825807
"report_issue" => "trusted".dark_green().bold(),
826808
"introspect" => "trusted".dark_green().bold(),
@@ -959,6 +941,7 @@ mod tests {
959941
use serde_json::json;
960942

961943
use super::*;
944+
use crate::cli::agent::hook::Source;
962945
const INPUT: &str = r#"
963946
{
964947
"name": "some_agent",
@@ -968,21 +951,21 @@ mod tests {
968951
"fetch": { "command": "fetch3.1", "args": [] },
969952
"git": { "command": "git-mcp", "args": [] }
970953
},
971-
"tools": [
954+
"tools": [
972955
"@git"
973956
],
974957
"toolAliases": {
975958
"@gits/some_tool": "some_tool2"
976959
},
977-
"allowedTools": [
978-
"fs_read",
960+
"allowedTools": [
961+
"fs_read",
979962
"@fetch",
980963
"@gits/git_status"
981964
],
982-
"resources": [
965+
"resources": [
983966
"file://~/my-genai-prompts/unittest.md"
984967
],
985-
"toolsSettings": {
968+
"toolsSettings": {
986969
"fs_write": { "allowedPaths": ["~/**"] },
987970
"@git/git_status": { "git_user": "$GIT_USER" }
988971
}
@@ -1188,8 +1171,8 @@ mod tests {
11881171
let execute_name = if cfg!(windows) { "execute_cmd" } else { "execute_bash" };
11891172
let execute_bash_label = agents.display_label(execute_name, &ToolOrigin::Native);
11901173
assert!(
1191-
execute_bash_label.contains("read-only"),
1192-
"execute_bash should show read-only by default, instead found: {}",
1174+
execute_bash_label.contains("not trusted"),
1175+
"execute_bash should not be trusted by default, instead found: {}",
11931176
execute_bash_label
11941177
);
11951178
}
@@ -1353,4 +1336,70 @@ mod tests {
13531336

13541337
assert_eq!(agents.get_active().and_then(|a| a.model.as_ref()), None);
13551338
}
1339+
1340+
#[test]
1341+
fn test_agent_with_hooks() {
1342+
let agent_json = json!({
1343+
"name": "test-agent",
1344+
"hooks": {
1345+
"agentSpawn": [
1346+
{
1347+
"command": "git status"
1348+
}
1349+
],
1350+
"preToolUse": [
1351+
{
1352+
"matcher": "fs_write",
1353+
"command": "validate-tool.sh"
1354+
},
1355+
{
1356+
"matcher": "fs_read",
1357+
"command": "enforce-tdd.sh"
1358+
}
1359+
],
1360+
"postToolUse": [
1361+
{
1362+
"matcher": "fs_write",
1363+
"command": "format-python.sh"
1364+
}
1365+
]
1366+
}
1367+
});
1368+
1369+
let agent: Agent = serde_json::from_value(agent_json).expect("Failed to deserialize agent");
1370+
1371+
// Verify agent name
1372+
assert_eq!(agent.name, "test-agent");
1373+
1374+
// Verify agentSpawn hook
1375+
assert!(agent.hooks.contains_key(&HookTrigger::AgentSpawn));
1376+
let agent_spawn_hooks = &agent.hooks[&HookTrigger::AgentSpawn];
1377+
assert_eq!(agent_spawn_hooks.len(), 1);
1378+
assert_eq!(agent_spawn_hooks[0].command, "git status");
1379+
assert_eq!(agent_spawn_hooks[0].matcher, None);
1380+
1381+
// Verify preToolUse hooks
1382+
assert!(agent.hooks.contains_key(&HookTrigger::PreToolUse));
1383+
let pre_tool_hooks = &agent.hooks[&HookTrigger::PreToolUse];
1384+
assert_eq!(pre_tool_hooks.len(), 2);
1385+
1386+
assert_eq!(pre_tool_hooks[0].command, "validate-tool.sh");
1387+
assert_eq!(pre_tool_hooks[0].matcher, Some("fs_write".to_string()));
1388+
1389+
assert_eq!(pre_tool_hooks[1].command, "enforce-tdd.sh");
1390+
assert_eq!(pre_tool_hooks[1].matcher, Some("fs_read".to_string()));
1391+
1392+
// Verify postToolUse hooks
1393+
assert!(agent.hooks.contains_key(&HookTrigger::PostToolUse));
1394+
1395+
// Verify default values are set correctly
1396+
for hooks in agent.hooks.values() {
1397+
for hook in hooks {
1398+
assert_eq!(hook.timeout_ms, 30_000);
1399+
assert_eq!(hook.max_output_size, 10_240);
1400+
assert_eq!(hook.cache_ttl_seconds, 0);
1401+
assert_eq!(hook.source, Source::Agent);
1402+
}
1403+
}
1404+
}
13561405
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ impl ClearArgs {
5252
if let Some(cm) = session.conversation.context_manager.as_mut() {
5353
cm.hook_executor.cache.clear();
5454
}
55+
56+
// Reset pending tool state to prevent orphaned tool approval prompts
57+
session.tool_uses.clear();
58+
session.pending_tool_index = None;
59+
session.tool_turn_start_time = None;
60+
5561
execute!(
5662
session.stderr,
5763
style::SetForegroundColor(Color::Green),

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

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,8 @@ impl EditorArgs {
8484
}
8585
}
8686

87-
/// Opens the user's preferred editor to compose a prompt
88-
pub fn open_editor(initial_text: Option<String>) -> Result<String, ChatError> {
89-
// Create a temporary file with a unique name
90-
let temp_dir = std::env::temp_dir();
91-
let file_name = format!("q_prompt_{}.md", Uuid::new_v4());
92-
let temp_file_path = temp_dir.join(file_name);
93-
87+
/// Launch the user's preferred editor with the given file path
88+
fn launch_editor(file_path: &std::path::Path) -> Result<(), ChatError> {
9489
// Get the editor from environment variable or use a default
9590
let editor_cmd = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
9691

@@ -104,11 +99,6 @@ pub fn open_editor(initial_text: Option<String>) -> Result<String, ChatError> {
10499

105100
let editor_bin = parts.remove(0);
106101

107-
// Write initial content to the file if provided
108-
let initial_content = initial_text.unwrap_or_default();
109-
std::fs::write(&temp_file_path, &initial_content)
110-
.map_err(|e| ChatError::Custom(format!("Failed to create temporary file: {}", e).into()))?;
111-
112102
// Open the editor with the parsed command and arguments
113103
let mut cmd = std::process::Command::new(editor_bin);
114104
// Add any arguments that were part of the EDITOR variable
@@ -117,14 +107,37 @@ pub fn open_editor(initial_text: Option<String>) -> Result<String, ChatError> {
117107
}
118108
// Add the file path as the last argument
119109
let status = cmd
120-
.arg(&temp_file_path)
110+
.arg(file_path)
121111
.status()
122112
.map_err(|e| ChatError::Custom(format!("Failed to open editor: {}", e).into()))?;
123113

124114
if !status.success() {
125115
return Err(ChatError::Custom("Editor exited with non-zero status".into()));
126116
}
127117

118+
Ok(())
119+
}
120+
121+
/// Opens the user's preferred editor to edit an existing file
122+
pub fn open_editor_file(file_path: &std::path::Path) -> Result<(), ChatError> {
123+
launch_editor(file_path)
124+
}
125+
126+
/// Opens the user's preferred editor to compose a prompt
127+
pub fn open_editor(initial_text: Option<String>) -> Result<String, ChatError> {
128+
// Create a temporary file with a unique name
129+
let temp_dir = std::env::temp_dir();
130+
let file_name = format!("q_prompt_{}.md", Uuid::new_v4());
131+
let temp_file_path = temp_dir.join(file_name);
132+
133+
// Write initial content to the file if provided
134+
let initial_content = initial_text.unwrap_or_default();
135+
std::fs::write(&temp_file_path, &initial_content)
136+
.map_err(|e| ChatError::Custom(format!("Failed to create temporary file: {}", e).into()))?;
137+
138+
// Launch the editor
139+
launch_editor(&temp_file_path)?;
140+
128141
// Read the content back
129142
let content = std::fs::read_to_string(&temp_file_path)
130143
.map_err(|e| ChatError::Custom(format!("Failed to read temporary file: {}", e).into()))?;

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ static AVAILABLE_EXPERIMENTS: &[Experiment] = &[
5959
),
6060
setting_key: Setting::EnabledCheckpoint,
6161
},
62+
Experiment {
63+
name: "Context Usage Indicator",
64+
description: "Shows context usage percentage in the prompt (e.g., [rust-agent] 6% >)",
65+
setting_key: Setting::EnabledContextUsageIndicator,
66+
},
6267
];
6368

6469
#[derive(Debug, PartialEq, Args)]

0 commit comments

Comments
 (0)