Skip to content

Commit 1384e6a

Browse files
authored
feat(commands): granular permissions with /tools (#1054)
Implements #921 which describes trusting and untrusting specific tools for the current session. - Start with default permissions. Users can change them from command line or within chat via /tools - `/acceptall` and `--accept-all` were deprecated in favor of `--trust-all-tools` and `/tools trustall`. They will continue to work, but display a notice and activation functionality of the new commands. - Command::AcceptAll no longer exists - UI has been reword a little, see screenshots. - `report_issue` will include trust override settings in the report. - Chat supports multiple tool use requests from Q at once. I have refactored the tool flow to allow asking permissions for individual tool requests at a time - Now, ExecuteTools (checks for acceptance one tool at a time) -> PromptUser (ask for acceptance) -> HandleInput (handle acceptance) -> ExecuteTools (find next tool that needs acceptance OR trigger execution if none remaining). - I was not able to find a prompt that made Q send multiple tool_uses in a single request coming from Q. It seems to do them iteratively even when asked to do them in parallel. - In any case, that original logic to support parallel requests is preserved with this new structure. Risks: May conflict with #999
1 parent 55053d5 commit 1384e6a

File tree

7 files changed

+609
-112
lines changed

7 files changed

+609
-112
lines changed

crates/q_cli/src/cli/chat/command.rs

Lines changed: 127 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
use std::io::Write;
2+
3+
use crossterm::style::Color;
4+
use crossterm::{
5+
queue,
6+
style,
7+
};
18
use eyre::Result;
29

310
#[derive(Debug, PartialEq, Eq)]
@@ -6,11 +13,11 @@ pub enum Command {
613
Execute { command: String },
714
Clear,
815
Help,
9-
AcceptAll,
1016
Issue { prompt: Option<String> },
1117
Quit,
1218
Profile { subcommand: ProfileSubcommand },
1319
Context { subcommand: ContextSubcommand },
20+
Tools { subcommand: Option<ToolsSubcommand> },
1421
}
1522

1623
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -126,6 +133,56 @@ Adding relevant files to your context helps Amazon Q provide more accurate and h
126133
}
127134
}
128135

136+
#[derive(Debug, Clone, PartialEq, Eq)]
137+
pub enum ToolsSubcommand {
138+
Trust { tool_name: String },
139+
Untrust { tool_name: String },
140+
TrustAll,
141+
Reset,
142+
Help,
143+
}
144+
145+
impl ToolsSubcommand {
146+
const AVAILABLE_COMMANDS: &str = color_print::cstr! {"<cyan!>Available subcommands</cyan!>
147+
<em>help</em> <black!>Show an explanation for the tools command</black!>
148+
<em>trust <<tool name>></em> <black!>Trust a specific tool for the session</black!>
149+
<em>untrust <<tool name>></em> <black!>Revert a tool to per-request confirmation</black!>
150+
<em>trustall</em> <black!>Trust all tools (equivalent to deprecated /acceptall)</black!>
151+
<em>reset</em> <black!>Reset all tools to default permission levels</black!>"};
152+
const BASE_COMMAND: &str = color_print::cstr! {"<cyan!>Usage: /tools [SUBCOMMAND]</cyan!>
153+
154+
<cyan!>Description</cyan!>
155+
Show the current set of tools and their permission settings.
156+
Alternatively, specify a subcommand to modify the tool permissions."};
157+
const TRUST_USAGE: &str = "/tools trust <tool name>";
158+
const UNTRUST_USAGE: &str = "/tools untrust <tool name>";
159+
160+
fn usage_msg(header: impl AsRef<str>) -> String {
161+
format!(
162+
"{}\n\n{}\n\n{}",
163+
header.as_ref(),
164+
Self::BASE_COMMAND,
165+
Self::AVAILABLE_COMMANDS
166+
)
167+
}
168+
169+
pub fn help_text() -> String {
170+
color_print::cformat!(
171+
r#"
172+
<magenta,em>Tool Permissions</magenta,em>
173+
174+
By default, Amazon Q will ask for your permission to use certain tools. You can control which tools you
175+
trust so that no confirmation is required. These settings will last only for this session.
176+
177+
{}
178+
179+
{}"#,
180+
Self::BASE_COMMAND,
181+
Self::AVAILABLE_COMMANDS
182+
)
183+
}
184+
}
185+
129186
impl Command {
130187
// Check if input is a common single-word command that should use slash prefix
131188
fn check_common_command(input: &str) -> Option<String> {
@@ -145,7 +202,7 @@ impl Command {
145202
}
146203
}
147204

148-
pub fn parse(input: &str) -> Result<Self, String> {
205+
pub fn parse(input: &str, output: &mut impl Write) -> Result<Self, String> {
149206
let input = input.trim();
150207

151208
// Check for common single-word commands without slash prefix
@@ -163,7 +220,18 @@ impl Command {
163220
return Ok(match parts[0].to_lowercase().as_str() {
164221
"clear" => Self::Clear,
165222
"help" => Self::Help,
166-
"acceptall" => Self::AcceptAll,
223+
"acceptall" => {
224+
let _ = queue!(
225+
output,
226+
style::SetForegroundColor(Color::Yellow),
227+
style::Print("\n/acceptall is deprecated. Use /tools instead.\n\n"),
228+
style::SetForegroundColor(Color::Reset)
229+
);
230+
231+
Self::Tools {
232+
subcommand: Some(ToolsSubcommand::TrustAll),
233+
}
234+
},
167235
"issue" => {
168236
if parts.len() > 1 {
169237
Self::Issue {
@@ -346,6 +414,57 @@ impl Command {
346414
},
347415
}
348416
},
417+
"tools" => {
418+
if parts.len() < 2 {
419+
return Ok(Self::Tools { subcommand: None });
420+
}
421+
422+
macro_rules! usage_err {
423+
($subcommand:expr, $usage_str:expr) => {
424+
return Err(format!(
425+
"Invalid /tools {} arguments.\n\nUsage:\n {}",
426+
$subcommand, $usage_str
427+
))
428+
};
429+
}
430+
431+
match parts[1].to_lowercase().as_str() {
432+
"trust" => {
433+
let tool_name = parts.get(2);
434+
match tool_name {
435+
Some(tool_name) => Self::Tools {
436+
subcommand: Some(ToolsSubcommand::Trust {
437+
tool_name: (*tool_name).to_string(),
438+
}),
439+
},
440+
None => usage_err!("trust", ToolsSubcommand::TRUST_USAGE),
441+
}
442+
},
443+
"untrust" => {
444+
let tool_name = parts.get(2);
445+
match tool_name {
446+
Some(tool_name) => Self::Tools {
447+
subcommand: Some(ToolsSubcommand::Untrust {
448+
tool_name: (*tool_name).to_string(),
449+
}),
450+
},
451+
None => usage_err!("untrust", ToolsSubcommand::UNTRUST_USAGE),
452+
}
453+
},
454+
"trustall" => Self::Tools {
455+
subcommand: Some(ToolsSubcommand::TrustAll),
456+
},
457+
"reset" => Self::Tools {
458+
subcommand: Some(ToolsSubcommand::Reset),
459+
},
460+
"help" => Self::Tools {
461+
subcommand: Some(ToolsSubcommand::Help),
462+
},
463+
other => {
464+
return Err(ToolsSubcommand::usage_msg(format!("Unknown subcommand '{}'.", other)));
465+
},
466+
}
467+
},
349468
_ => {
350469
return Ok(Self::Ask {
351470
prompt: input.to_string(),
@@ -372,6 +491,8 @@ mod tests {
372491

373492
#[test]
374493
fn test_command_parse() {
494+
let mut stdout = std::io::stdout();
495+
375496
macro_rules! profile {
376497
($subcommand:expr) => {
377498
Command::Profile {
@@ -463,12 +584,13 @@ mod tests {
463584
];
464585

465586
for (input, parsed) in tests {
466-
assert_eq!(&Command::parse(input).unwrap(), parsed, "{}", input);
587+
assert_eq!(&Command::parse(input, &mut stdout).unwrap(), parsed, "{}", input);
467588
}
468589
}
469590

470591
#[test]
471592
fn test_common_command_suggestions() {
593+
let mut stdout = std::io::stdout();
472594
let test_cases = vec![
473595
(
474596
"exit",
@@ -501,7 +623,7 @@ mod tests {
501623
];
502624

503625
for (input, expected_message) in test_cases {
504-
let result = Command::parse(input);
626+
let result = Command::parse(input, &mut stdout);
505627
assert!(result.is_err(), "Expected error for input: {}", input);
506628
assert_eq!(result.unwrap_err(), expected_message);
507629
}

crates/q_cli/src/cli/chat/conversation_state.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ use tracing::{
3333
};
3434

3535
use super::context::ContextManager;
36-
use super::tools::ToolSpec;
36+
use super::tools::{
37+
QueuedTool,
38+
ToolSpec,
39+
};
3740
use super::truncate_safe;
3841
use crate::cli::chat::tools::{
3942
InputSchema,
@@ -64,7 +67,7 @@ pub struct ConversationState {
6467
/// e.g user messages prefixed with '> '. Should also be used to store errors posted in the
6568
/// chat.
6669
pub transcript: VecDeque<String>,
67-
tools: Vec<Tool>,
70+
pub tools: Vec<Tool>,
6871
/// Context manager for handling sticky context files
6972
pub context_manager: Option<ContextManager>,
7073
/// Cached value representing the length of the user context message.
@@ -350,12 +353,12 @@ impl ConversationState {
350353
}
351354

352355
/// Sets the next user message with "cancelled" tool results.
353-
pub fn abandon_tool_use(&mut self, tools_to_be_abandoned: Vec<(String, super::tools::Tool)>, deny_input: String) {
356+
pub fn abandon_tool_use(&mut self, tools_to_be_abandoned: Vec<QueuedTool>, deny_input: String) {
354357
debug_assert!(self.next_message.is_none());
355358
let tool_results = tools_to_be_abandoned
356359
.into_iter()
357-
.map(|(tool_use_id, _)| ToolResult {
358-
tool_use_id,
360+
.map(|tool| ToolResult {
361+
tool_use_id: tool.id,
359362
content: vec![ToolResultContentBlock::Text(
360363
"Tool use was cancelled by the user".to_string(),
361364
)],

0 commit comments

Comments
 (0)