diff --git a/server/src/agents/pty.rs b/server/src/agents/pty.rs index 62c7b572..8b551bfd 100644 --- a/server/src/agents/pty.rs +++ b/server/src/agents/pty.rs @@ -198,9 +198,13 @@ fn run_agent_pty_blocking( // and instead leak as unstructured PTY text. cmd.arg("--include-partial-messages"); - // Supervised agents don't need interactive permission prompts + // Agents use allowFullAutoEdit so the worktree's .claude/settings.json + // controls which tools are pre-approved. Anything not in the allowlist + // triggers the permission prompt tool, which auto-denies for agents. cmd.arg("--permission-mode"); - cmd.arg("bypassPermissions"); + cmd.arg("allowFullAutoEdit"); + cmd.arg("--permission-prompt-tool"); + cmd.arg("mcp__huskies__prompt_permission"); cmd.cwd(cwd); cmd.env("NO_COLOR", "1"); diff --git a/server/src/http/mcp/diagnostics.rs b/server/src/http/mcp/diagnostics.rs index f010d933..307f3996 100644 --- a/server/src/http/mcp/diagnostics.rs +++ b/server/src/http/mcp/diagnostics.rs @@ -150,14 +150,26 @@ pub(super) async fn tool_prompt_permission( let request_id = uuid::Uuid::new_v4().to_string(); let (response_tx, response_rx) = tokio::sync::oneshot::channel(); - ctx.perm_tx + // Try to forward to the interactive session (WebSocket/Matrix). + // If no session is active (headless agent), auto-deny the permission. + if ctx.perm_tx .send(crate::http::context::PermissionForward { request_id: request_id.clone(), tool_name: tool_name.clone(), tool_input: tool_input.clone(), response_tx, }) - .map_err(|_| "No active WebSocket session to receive permission request".to_string())?; + .is_err() + { + crate::slog!( + "[permission] Auto-denied '{tool_name}' (no interactive session — agent mode)" + ); + return serde_json::to_string_pretty(&json!({ + "behavior": "deny", + "message": format!("Permission denied for '{tool_name}'. Use the appropriate MCP tool instead (e.g. run_tests, run_build, run_lint).") + })) + .map_err(|e| format!("Serialization error: {e}")); + } use crate::http::context::PermissionDecision;