fix: prevent session hang when tool approval required in headless mode (#7915)

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vincenzo Palazzo
2026-04-15 11:58:27 +02:00
committed by GitHub
parent 529d6f7195
commit b7f1316ab8
2 changed files with 40 additions and 5 deletions
@@ -268,7 +268,7 @@ where
let mut error = None;
for message in &messages {
if let Err(e) = cli_session
.process_message(message.clone(), CancellationToken::default())
.process_message(message.clone(), CancellationToken::default(), false)
.await
{
error = Some(e.to_string());
+39 -4
View File
@@ -488,10 +488,12 @@ impl CliSession {
&mut self,
message: Message,
cancel_token: CancellationToken,
interactive: bool,
) -> Result<()> {
let cancel_token = cancel_token.clone();
self.push_message(message);
self.process_agent_response(false, cancel_token).await?;
self.process_agent_response(interactive, cancel_token)
.await?;
Ok(())
}
@@ -499,7 +501,7 @@ impl CliSession {
pub async fn interactive(&mut self, prompt: Option<String>) -> Result<()> {
if let Some(prompt) = prompt {
let msg = Message::user().with_text(&prompt);
self.process_message(msg, CancellationToken::default())
self.process_message(msg, CancellationToken::default(), true)
.await?;
}
@@ -955,7 +957,7 @@ impl CliSession {
/// Process a single message and exit
pub async fn headless(&mut self, prompt: String) -> Result<()> {
let message = Message::user().with_text(&prompt);
self.process_message(message, CancellationToken::default())
self.process_message(message, CancellationToken::default(), false)
.await?;
Ok(())
}
@@ -1009,7 +1011,28 @@ impl CliSession {
match result {
Some(Ok(AgentEvent::Message(message))) => {
if let Some((id, security_prompt)) = find_tool_confirmation(&message) {
let permission = prompt_tool_confirmation(&security_prompt)?;
let permission = if interactive {
prompt_tool_confirmation(&security_prompt)?
} else {
// Non-interactive/headless mode: refuse to run in
// Approve/SmartApprove modes since auto-allowing would
// bypass the safety contract those modes are meant to enforce.
let config = Config::global();
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
if goose_mode == GooseMode::Approve || goose_mode == GooseMode::SmartApprove {
cancel_token_clone.cancel();
drop(stream);
return Err(anyhow::anyhow!(
"Tool approval required in non-interactive mode with GooseMode::{goose_mode}. \
This is an invalid configuration — Approve/SmartApprove modes require an \
interactive terminal. Use GooseMode::Auto for headless sessions."
));
}
tracing::warn!(
"Tool confirmation required in non-interactive mode, auto-allowing"
);
Permission::AllowOnce
};
if permission == Permission::Cancel {
output::render_text("Tool call cancelled. Returning to chat...", Some(Color::Yellow), true);
@@ -1036,6 +1059,18 @@ impl CliSession {
permission,
}).await;
} else if let Some((elicitation_id, elicitation_message, schema)) = find_elicitation_request(&message) {
if !interactive {
// Non-interactive/headless mode: cannot collect user input
tracing::warn!(
"Elicitation requested in non-interactive mode, cancelling"
);
cancel_token_clone.cancel();
drop(stream);
return Err(anyhow::anyhow!(
"Elicitation requested but no interactive terminal is available to collect user input"
));
}
output::hide_thinking();
let _ = progress_bars.hide();