Skip to content

Commit eca3969

Browse files
authored
Merge branch 'aws:main' into main
2 parents bf613f9 + bf183f0 commit eca3969

File tree

31 files changed

+1893
-343
lines changed

31 files changed

+1893
-343
lines changed

.github/CODEOWNERS

Lines changed: 0 additions & 1 deletion
This file was deleted.

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.15.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: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -815,12 +815,12 @@ impl Agents {
815815
// This "static" way avoids needing to construct a tool instance.
816816
fn default_permission_label(&self, tool_name: &str) -> String {
817817
let label = match tool_name {
818-
"fs_read" => "trusted".dark_green().bold(),
818+
"fs_read" => "trust working directory".dark_grey(),
819819
"fs_write" => "not trusted".dark_grey(),
820820
#[cfg(not(windows))]
821-
"execute_bash" => "trust read-only commands".dark_grey(),
821+
"execute_bash" => "not trusted".dark_grey(),
822822
#[cfg(windows)]
823-
"execute_cmd" => "trust read-only commands".dark_grey(),
823+
"execute_cmd" => "not trusted".dark_grey(),
824824
"use_aws" => "trust read-only commands".dark_grey(),
825825
"report_issue" => "trusted".dark_green().bold(),
826826
"introspect" => "trusted".dark_green().bold(),
@@ -959,6 +959,7 @@ mod tests {
959959
use serde_json::json;
960960

961961
use super::*;
962+
use crate::cli::agent::hook::Source;
962963
const INPUT: &str = r#"
963964
{
964965
"name": "some_agent",
@@ -968,21 +969,21 @@ mod tests {
968969
"fetch": { "command": "fetch3.1", "args": [] },
969970
"git": { "command": "git-mcp", "args": [] }
970971
},
971-
"tools": [
972+
"tools": [
972973
"@git"
973974
],
974975
"toolAliases": {
975976
"@gits/some_tool": "some_tool2"
976977
},
977-
"allowedTools": [
978-
"fs_read",
978+
"allowedTools": [
979+
"fs_read",
979980
"@fetch",
980981
"@gits/git_status"
981982
],
982-
"resources": [
983+
"resources": [
983984
"file://~/my-genai-prompts/unittest.md"
984985
],
985-
"toolsSettings": {
986+
"toolsSettings": {
986987
"fs_write": { "allowedPaths": ["~/**"] },
987988
"@git/git_status": { "git_user": "$GIT_USER" }
988989
}
@@ -1142,9 +1143,9 @@ mod tests {
11421143

11431144
let label = agents.display_label("fs_read", &ToolOrigin::Native);
11441145
// With no active agent, it should fall back to default permissions
1145-
// fs_read has a default of "trusted"
1146+
// fs_read has a default of "trust working directory"
11461147
assert!(
1147-
label.contains("trusted"),
1148+
label.contains("trust working directory"),
11481149
"fs_read should show default trusted permission, instead found: {}",
11491150
label
11501151
);
@@ -1173,7 +1174,7 @@ mod tests {
11731174
// Test default permissions for known tools
11741175
let fs_read_label = agents.display_label("fs_read", &ToolOrigin::Native);
11751176
assert!(
1176-
fs_read_label.contains("trusted"),
1177+
fs_read_label.contains("trust working directory"),
11771178
"fs_read should be trusted by default, instead found: {}",
11781179
fs_read_label
11791180
);
@@ -1188,8 +1189,8 @@ mod tests {
11881189
let execute_name = if cfg!(windows) { "execute_cmd" } else { "execute_bash" };
11891190
let execute_bash_label = agents.display_label(execute_name, &ToolOrigin::Native);
11901191
assert!(
1191-
execute_bash_label.contains("read-only"),
1192-
"execute_bash should show read-only by default, instead found: {}",
1192+
execute_bash_label.contains("not trusted"),
1193+
"execute_bash should not be trusted by default, instead found: {}",
11931194
execute_bash_label
11941195
);
11951196
}
@@ -1353,4 +1354,70 @@ mod tests {
13531354

13541355
assert_eq!(agents.get_active().and_then(|a| a.model.as_ref()), None);
13551356
}
1357+
1358+
#[test]
1359+
fn test_agent_with_hooks() {
1360+
let agent_json = json!({
1361+
"name": "test-agent",
1362+
"hooks": {
1363+
"agentSpawn": [
1364+
{
1365+
"command": "git status"
1366+
}
1367+
],
1368+
"preToolUse": [
1369+
{
1370+
"matcher": "fs_write",
1371+
"command": "validate-tool.sh"
1372+
},
1373+
{
1374+
"matcher": "fs_read",
1375+
"command": "enforce-tdd.sh"
1376+
}
1377+
],
1378+
"postToolUse": [
1379+
{
1380+
"matcher": "fs_write",
1381+
"command": "format-python.sh"
1382+
}
1383+
]
1384+
}
1385+
});
1386+
1387+
let agent: Agent = serde_json::from_value(agent_json).expect("Failed to deserialize agent");
1388+
1389+
// Verify agent name
1390+
assert_eq!(agent.name, "test-agent");
1391+
1392+
// Verify agentSpawn hook
1393+
assert!(agent.hooks.contains_key(&HookTrigger::AgentSpawn));
1394+
let agent_spawn_hooks = &agent.hooks[&HookTrigger::AgentSpawn];
1395+
assert_eq!(agent_spawn_hooks.len(), 1);
1396+
assert_eq!(agent_spawn_hooks[0].command, "git status");
1397+
assert_eq!(agent_spawn_hooks[0].matcher, None);
1398+
1399+
// Verify preToolUse hooks
1400+
assert!(agent.hooks.contains_key(&HookTrigger::PreToolUse));
1401+
let pre_tool_hooks = &agent.hooks[&HookTrigger::PreToolUse];
1402+
assert_eq!(pre_tool_hooks.len(), 2);
1403+
1404+
assert_eq!(pre_tool_hooks[0].command, "validate-tool.sh");
1405+
assert_eq!(pre_tool_hooks[0].matcher, Some("fs_write".to_string()));
1406+
1407+
assert_eq!(pre_tool_hooks[1].command, "enforce-tdd.sh");
1408+
assert_eq!(pre_tool_hooks[1].matcher, Some("fs_read".to_string()));
1409+
1410+
// Verify postToolUse hooks
1411+
assert!(agent.hooks.contains_key(&HookTrigger::PostToolUse));
1412+
1413+
// Verify default values are set correctly
1414+
for hooks in agent.hooks.values() {
1415+
for hook in hooks {
1416+
assert_eq!(hook.timeout_ms, 30_000);
1417+
assert_eq!(hook.max_output_size, 10_240);
1418+
assert_eq!(hook.cache_ttl_seconds, 0);
1419+
assert_eq!(hook.source, Source::Agent);
1420+
}
1421+
}
1422+
}
13561423
}

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),

0 commit comments

Comments
 (0)