Skip to content

Commit 515b4df

Browse files
authored
Trim region to avoid login failures (#2930)
1 parent 61efff3 commit 515b4df

File tree

7 files changed

+185
-133
lines changed

7 files changed

+185
-133
lines changed

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

Lines changed: 75 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,14 @@ use crate::cli::agent::hook::{
3838
HookTrigger,
3939
};
4040
use crate::cli::agent::is_mcp_tool_ref;
41-
use crate::util::MCP_SERVER_TOOL_DELIMITER;
4241
use crate::cli::chat::consts::AGENT_FORMAT_HOOKS_DOC_URL;
4342
use crate::cli::chat::util::truncate_safe;
4443
use crate::cli::chat::{
4544
ChatError,
4645
ChatSession,
4746
ChatState,
4847
};
48+
use crate::util::MCP_SERVER_TOOL_DELIMITER;
4949
use crate::util::pattern_matching::matches_any_pattern;
5050

5151
/// Hook execution result: (exit_code, output)
@@ -58,26 +58,29 @@ fn hook_matches_tool(hook: &Hook, tool_name: &str) -> bool {
5858
None => true, // No matcher means the hook runs for all tools
5959
Some(pattern) => {
6060
match pattern.as_str() {
61-
"*" => true, // Wildcard matches all tools
61+
"*" => true, // Wildcard matches all tools
6262
"@builtin" => !is_mcp_tool_ref(tool_name), // Built-in tools are not MCP tools
6363
_ => {
6464
// If tool_name is MCP, check server pattern first
6565
if is_mcp_tool_ref(tool_name) {
66-
if let Some(server_name) = tool_name.strip_prefix('@').and_then(|s| s.split(MCP_SERVER_TOOL_DELIMITER).next()) {
66+
if let Some(server_name) = tool_name
67+
.strip_prefix('@')
68+
.and_then(|s| s.split(MCP_SERVER_TOOL_DELIMITER).next())
69+
{
6770
let server_pattern = format!("@{}", server_name);
6871
if pattern == &server_pattern {
6972
return true;
7073
}
7174
}
7275
}
73-
76+
7477
// Use matches_any_pattern for both MCP and built-in tools
7578
let mut patterns = std::collections::HashSet::new();
7679
patterns.insert(pattern.clone());
7780
matches_any_pattern(&patterns, tool_name)
78-
}
81+
},
7982
}
80-
}
83+
},
8184
}
8285
}
8386

@@ -133,7 +136,7 @@ impl HookExecutor {
133136
continue; // Skip this hook - doesn't match tool
134137
}
135138
}
136-
139+
137140
if let Some(cache) = self.get_cache(&hook) {
138141
// Note: we only cache successful hook run. hence always using 0 as exit code for cached hook
139142
cached.push((hook.clone(), (0, cache)));
@@ -203,7 +206,11 @@ impl HookExecutor {
203206
style::Print(&hook.1.command),
204207
style::Print("\""),
205208
style::SetForegroundColor(style::Color::Red),
206-
style::Print(format!(" failed with exit code: {}, stderr: {})\n", exit_code, hook_output.trim_end())),
209+
style::Print(format!(
210+
" failed with exit code: {}, stderr: {})\n",
211+
exit_code,
212+
hook_output.trim_end()
213+
)),
207214
style::ResetColor,
208215
)?;
209216
} else {
@@ -437,11 +444,16 @@ impl HooksArgs {
437444

438445
#[cfg(test)]
439446
mod tests {
440-
use super::*;
441447
use std::collections::HashMap;
442-
use crate::cli::agent::hook::{Hook, HookTrigger};
448+
443449
use tempfile::TempDir;
444450

451+
use super::*;
452+
use crate::cli::agent::hook::{
453+
Hook,
454+
HookTrigger,
455+
};
456+
445457
#[test]
446458
fn test_hook_matches_tool() {
447459
let hook_no_matcher = Hook {
@@ -452,7 +464,7 @@ mod tests {
452464
matcher: None,
453465
source: crate::cli::agent::hook::Source::Session,
454466
};
455-
467+
456468
let fs_write_hook = Hook {
457469
command: "echo test".to_string(),
458470
timeout_ms: 5000,
@@ -461,7 +473,7 @@ mod tests {
461473
matcher: Some("fs_write".to_string()),
462474
source: crate::cli::agent::hook::Source::Session,
463475
};
464-
476+
465477
let fs_wildcard_hook = Hook {
466478
command: "echo test".to_string(),
467479
timeout_ms: 5000,
@@ -470,7 +482,7 @@ mod tests {
470482
matcher: Some("fs_*".to_string()),
471483
source: crate::cli::agent::hook::Source::Session,
472484
};
473-
485+
474486
let all_tools_hook = Hook {
475487
command: "echo test".to_string(),
476488
timeout_ms: 5000,
@@ -479,7 +491,7 @@ mod tests {
479491
matcher: Some("*".to_string()),
480492
source: crate::cli::agent::hook::Source::Session,
481493
};
482-
494+
483495
let builtin_hook = Hook {
484496
command: "echo test".to_string(),
485497
timeout_ms: 5000,
@@ -488,7 +500,7 @@ mod tests {
488500
matcher: Some("@builtin".to_string()),
489501
source: crate::cli::agent::hook::Source::Session,
490502
};
491-
503+
492504
let git_server_hook = Hook {
493505
command: "echo test".to_string(),
494506
timeout_ms: 5000,
@@ -497,7 +509,7 @@ mod tests {
497509
matcher: Some("@git".to_string()),
498510
source: crate::cli::agent::hook::Source::Session,
499511
};
500-
512+
501513
let git_status_hook = Hook {
502514
command: "echo test".to_string(),
503515
timeout_ms: 5000,
@@ -506,36 +518,36 @@ mod tests {
506518
matcher: Some("@git/status".to_string()),
507519
source: crate::cli::agent::hook::Source::Session,
508520
};
509-
521+
510522
// No matcher should match all tools
511523
assert!(hook_matches_tool(&hook_no_matcher, "fs_write"));
512524
assert!(hook_matches_tool(&hook_no_matcher, "execute_bash"));
513525
assert!(hook_matches_tool(&hook_no_matcher, "@git/status"));
514-
526+
515527
// Exact matcher should only match exact tool
516528
assert!(hook_matches_tool(&fs_write_hook, "fs_write"));
517529
assert!(!hook_matches_tool(&fs_write_hook, "fs_read"));
518-
530+
519531
// Wildcard matcher should match pattern
520532
assert!(hook_matches_tool(&fs_wildcard_hook, "fs_write"));
521533
assert!(hook_matches_tool(&fs_wildcard_hook, "fs_read"));
522534
assert!(!hook_matches_tool(&fs_wildcard_hook, "execute_bash"));
523-
535+
524536
// * should match all tools
525537
assert!(hook_matches_tool(&all_tools_hook, "fs_write"));
526538
assert!(hook_matches_tool(&all_tools_hook, "execute_bash"));
527539
assert!(hook_matches_tool(&all_tools_hook, "@git/status"));
528-
540+
529541
// @builtin should match built-in tools only
530542
assert!(hook_matches_tool(&builtin_hook, "fs_write"));
531543
assert!(hook_matches_tool(&builtin_hook, "execute_bash"));
532544
assert!(!hook_matches_tool(&builtin_hook, "@git/status"));
533-
545+
534546
// @git should match all git server tools
535547
assert!(hook_matches_tool(&git_server_hook, "@git/status"));
536548
assert!(!hook_matches_tool(&git_server_hook, "@other/tool"));
537549
assert!(!hook_matches_tool(&git_server_hook, "fs_write"));
538-
550+
539551
// @git/status should match exact MCP tool
540552
assert!(hook_matches_tool(&git_status_hook, "@git/status"));
541553
assert!(!hook_matches_tool(&git_status_hook, "@git/commit"));
@@ -546,18 +558,18 @@ mod tests {
546558
async fn test_hook_executor_with_tool_context() {
547559
let mut executor = HookExecutor::new();
548560
let mut output = Vec::new();
549-
561+
550562
// Create temp directory and file
551563
let temp_dir = TempDir::new().unwrap();
552564
let test_file = temp_dir.path().join("hook_output.json");
553565
let test_file_str = test_file.to_string_lossy();
554-
566+
555567
// Create a simple hook that writes JSON input to a file
556568
#[cfg(unix)]
557569
let command = format!("cat > {}", test_file_str);
558570
#[cfg(windows)]
559571
let command = format!("type > {}", test_file_str);
560-
572+
561573
let hook = Hook {
562574
command,
563575
timeout_ms: 5000,
@@ -566,10 +578,10 @@ mod tests {
566578
matcher: Some("fs_write".to_string()),
567579
source: crate::cli::agent::hook::Source::Session,
568580
};
569-
581+
570582
let mut hooks = HashMap::new();
571583
hooks.insert(HookTrigger::PreToolUse, vec![hook]);
572-
584+
573585
let tool_context = ToolContext {
574586
tool_name: "fs_write".to_string(),
575587
tool_input: serde_json::json!({
@@ -578,18 +590,14 @@ mod tests {
578590
}),
579591
tool_response: None,
580592
};
581-
593+
582594
// Run the hook
583-
let result = executor.run_hooks(
584-
hooks,
585-
&mut output,
586-
".",
587-
None,
588-
Some(tool_context)
589-
).await;
590-
595+
let result = executor
596+
.run_hooks(hooks, &mut output, ".", None, Some(tool_context))
597+
.await;
598+
591599
assert!(result.is_ok());
592-
600+
593601
// Verify the hook wrote the JSON input to the file
594602
if let Ok(content) = std::fs::read_to_string(&test_file) {
595603
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
@@ -605,7 +613,7 @@ mod tests {
605613
async fn test_hook_filtering_no_match() {
606614
let mut executor = HookExecutor::new();
607615
let mut output = Vec::new();
608-
616+
609617
// Hook that matches execute_bash (should NOT run for fs_write tool call)
610618
let execute_bash_hook = Hook {
611619
command: "echo 'should not run'".to_string(),
@@ -615,31 +623,33 @@ mod tests {
615623
matcher: Some("execute_bash".to_string()),
616624
source: crate::cli::agent::hook::Source::Session,
617625
};
618-
626+
619627
let mut hooks = HashMap::new();
620628
hooks.insert(HookTrigger::PostToolUse, vec![execute_bash_hook]);
621-
629+
622630
let tool_context = ToolContext {
623631
tool_name: "fs_write".to_string(),
624632
tool_input: serde_json::json!({"command": "create"}),
625633
tool_response: Some(serde_json::json!({"success": true})),
626634
};
627-
635+
628636
// Run the hooks
629-
let result = executor.run_hooks(
630-
hooks,
631-
&mut output,
632-
".", // cwd - using current directory for now
633-
None, // prompt - no user prompt for this test
634-
Some(tool_context)
635-
).await;
636-
637+
let result = executor
638+
.run_hooks(
639+
hooks,
640+
&mut output,
641+
".", // cwd - using current directory for now
642+
None, // prompt - no user prompt for this test
643+
Some(tool_context),
644+
)
645+
.await;
646+
637647
assert!(result.is_ok());
638648
let hook_results = result.unwrap();
639-
649+
640650
// Should run 0 hooks because matcher doesn't match tool_name
641651
assert_eq!(hook_results.len(), 0);
642-
652+
643653
// Output should be empty since no hooks ran
644654
assert!(output.is_empty());
645655
}
@@ -654,7 +664,7 @@ mod tests {
654664
let command = "echo 'Tool execution blocked by security policy' >&2; exit 2";
655665
#[cfg(windows)]
656666
let command = "echo Tool execution blocked by security policy 1>&2 & exit /b 2";
657-
667+
658668
let hook = Hook {
659669
command: command.to_string(),
660670
timeout_ms: 5000,
@@ -664,9 +674,7 @@ mod tests {
664674
source: crate::cli::agent::hook::Source::Session,
665675
};
666676

667-
let hooks = HashMap::from([
668-
(HookTrigger::PreToolUse, vec![hook])
669-
]);
677+
let hooks = HashMap::from([(HookTrigger::PreToolUse, vec![hook])]);
670678

671679
let tool_context = ToolContext {
672680
tool_name: "fs_write".to_string(),
@@ -677,17 +685,20 @@ mod tests {
677685
tool_response: None,
678686
};
679687

680-
let results = executor.run_hooks(
681-
hooks,
682-
&mut output,
683-
".", // cwd
684-
None, // prompt
685-
Some(tool_context)
686-
).await.unwrap();
688+
let results = executor
689+
.run_hooks(
690+
hooks,
691+
&mut output,
692+
".", // cwd
693+
None, // prompt
694+
Some(tool_context),
695+
)
696+
.await
697+
.unwrap();
687698

688699
// Should have one result
689700
assert_eq!(results.len(), 1);
690-
701+
691702
let ((trigger, _hook), (exit_code, hook_output)) = &results[0];
692703
assert_eq!(*trigger, HookTrigger::PreToolUse);
693704
assert_eq!(*exit_code, 2);

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ use serde::{
1414
Serializer,
1515
};
1616

17+
use super::cli::hooks::HookOutput;
1718
use super::cli::model::context_window_tokens;
1819
use super::util::drop_matched_context_files;
19-
use super::cli::hooks::HookOutput;
2020
use crate::cli::agent::Agent;
2121
use crate::cli::agent::hook::{
2222
Hook,
@@ -255,7 +255,9 @@ impl ContextManager {
255255
let mut hooks = self.hooks.clone();
256256
hooks.retain(|t, _| *t == trigger);
257257
let cwd = os.env.current_dir()?.to_string_lossy().to_string();
258-
self.hook_executor.run_hooks(hooks, output, &cwd, prompt, tool_context).await
258+
self.hook_executor
259+
.run_hooks(hooks, output, &cwd, prompt, tool_context)
260+
.await
259261
}
260262
}
261263

0 commit comments

Comments
 (0)