Skip to content

Commit 2aff4e7

Browse files
committed
merges main
2 parents 19c0dcd + 5486923 commit 2aff4e7

Some content is hidden

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

52 files changed

+6430
-594
lines changed

.github/CODEOWNERS

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

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 2 deletions
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.3"
1212
license = "MIT OR Apache-2.0"
1313

1414
[workspace.dependencies]
@@ -129,7 +129,7 @@ winnow = "=0.6.2"
129129
winreg = "0.55.0"
130130
schemars = "1.0.4"
131131
jsonschema = "0.30.0"
132-
rmcp = { version = "0.6.3", features = ["client", "transport-sse-client-reqwest", "reqwest", "transport-streamable-http-client-reqwest", "transport-child-process", "tower", "auth"] }
132+
rmcp = { version = "0.7.0", features = ["client", "transport-sse-client-reqwest", "reqwest", "transport-streamable-http-client-reqwest", "transport-child-process", "tower", "auth"] }
133133

134134
[workspace.lints.rust]
135135
future_incompatible = "warn"

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
}

0 commit comments

Comments
 (0)