Skip to content

Add tool execution guardrails with confirmation popup (#438)#565

Merged
itomek merged 5 commits intomainfrom
438-tool-execution-guardrails-confirmation-popup-before-dangerous-commands
Mar 19, 2026
Merged

Add tool execution guardrails with confirmation popup (#438)#565
itomek merged 5 commits intomainfrom
438-tool-execution-guardrails-confirmation-popup-before-dangerous-commands

Conversation

@itomek
Copy link
Collaborator

@itomek itomek commented Mar 18, 2026

Summary

  • Adds a blocking confirmation popup in the Agent UI before run_shell_command executes, so users can Allow, Deny, or Always Allow each shell command the agent wants to run
  • CLI path (gaia chat) is unaffected — auto-approves as before
  • Reuses the existing PermissionPrompt.tsx / GaiaNotification / notificationStore infrastructure already in the codebase

Architecture

Agent thread (sync)                  SSE consumer (async)          Frontend
────────────────────                 ────────────────────           ────────
_execute_tool("run_shell_command")
  → console.confirm_tool_execution()
    → emit {"type":"tool_confirm"}  ──→  yields SSE event    ──→  ChatView.onAgentEvent
    → threading.Event.wait(60s)                                   → checks localStorage
         ↑                                                        → shows PermissionPrompt
         │                           POST /api/chat/confirm  ←──  → user clicks Allow/Deny
         └── Event.set() ─────────── resolve_confirmation()
  → execute or return denial

Files Changed

File Change
agents/base/console.py confirm_tool_execution() on OutputHandler (default True)
agents/base/agent.py TOOLS_REQUIRING_CONFIRMATION + guardrail in _execute_tool()
ui/sse_handler.py Blocking confirm_tool_execution() + resolve_confirmation()
ui/server.py app.state.active_sse_handlers registry
ui/_chat_helpers.py Register/unregister handler; pass http_request
ui/routers/chat.py POST /api/chat/confirm endpoint
ui/models.py ToolConfirmRequest model
webui/src/types/index.ts tool_confirm event type + fields
webui/src/services/api.ts confirmToolExecution() + event routing
webui/src/components/ChatView.tsx Handle tool_confirm, localStorage auto-approve
webui/src/stores/notificationStore.ts HTTP fallback, Always Allow persistence

Files Reused (no changes)

  • PermissionPrompt.tsx — full modal UI with countdown, Allow/Deny/Always Allow, keyboard shortcuts
  • GaiaNotification type — already has tool, toolArgs, timeoutSeconds fields

Test plan

Manual (Agent UI):

  1. gaia chat --ui → ask "run ls /tmp" → confirm popup appears with command shown
  2. Click Allow → command executes, output visible in chat
  3. Click Deny → agent responds gracefully without executing
  4. Check Remember + Allow → reload page → same prompt skips popup
  5. Do nothing for 60 s → popup auto-closes, command denied with warning

CLI regression:

  • gaia chat → same shell command prompt → no popup, executes immediately

Fixes #438

@itomek itomek linked an issue Mar 18, 2026 that may be closed by this pull request
7 tasks
@github-actions github-actions bot added documentation Documentation changes dependencies Dependency updates devops DevOps/infrastructure changes agents Agent system changes jira Jira agent changes talk Talk agent changes chat Chat SDK changes mcp MCP integration changes rag RAG system changes llm LLM backend changes audio Audio (ASR/TTS) changes cli CLI changes eval Evaluation framework changes tests Test changes electron Electron app changes security Security-sensitive changes performance Performance-critical changes labels Mar 18, 2026
@itomek itomek changed the base branch from main to kalin/chat-ui March 18, 2026 17:07
@itomek itomek self-assigned this Mar 18, 2026
@itomek itomek marked this pull request as ready for review March 18, 2026 17:07
@itomek itomek requested a review from kovtcharov-amd as a code owner March 18, 2026 17:07
Base automatically changed from kalin/chat-ui to main March 18, 2026 17:43
@itomek itomek force-pushed the 438-tool-execution-guardrails-confirmation-popup-before-dangerous-commands branch from bbd0cfd to 4fcddd5 Compare March 19, 2026 17:12
@itomek
Copy link
Collaborator Author

itomek commented Mar 19, 2026

@claude Please do a full code review of this PR following the GAIA review checklist. Focus on: security vulnerabilities, code quality, architecture consistency, testing coverage, and documentation. Report all 🔴 Critical and 🟡 Important findings with suggested fixes.

@github-actions
Copy link

github-actions bot commented Mar 19, 2026

Claude finished @itomek's task —— View job


Code Review Complete

  • Read PR diff and changed files
  • Security vulnerabilities analysis
  • Code quality review
  • Architecture consistency check
  • Testing coverage evaluation
  • Documentation review
  • Compile findings report

Summary

This PR implements a tool execution guardrail system for the Agent UI, requiring user confirmation before run_shell_command executes. The architecture uses SSE events and threading.Event for blocking synchronization between the agent thread and frontend.

Overall Assessment: Well-designed implementation that follows GAIA patterns. The defense-in-depth approach (existing shell command whitelist + new confirmation popup) is excellent. A few issues need attention.


🔴 Critical Findings

1. Input Validation Gap on action Field

File: src/gaia/ui/models.py:362

action: str = Field(..., description="'allow' or 'deny'")

The action field accepts any string. A malicious or buggy client could send action: "allow\x00deny" or other unexpected values. The comparison allowed = request.action == "allow" at routers/chat.py:155 is case-sensitive and doesn't sanitize.

Fix: Use Literal type for strict validation:

from typing import Literal

class ToolConfirmRequest(BaseModel):
    session_id: str
    confirm_id: str
    action: Literal["allow", "deny"]
    remember: bool = False

2. Race Condition in Handler Registration

File: src/gaia/ui/_chat_helpers.py:238-241

if http_request is not None:
    _active = getattr(http_request.app.state, "active_sse_handlers", None)
    if _active is not None:
        _active[request.session_id] = sse_handler

If a user rapidly sends two messages for the same session (bypassing the 0.5s session lock timeout), the second request could overwrite the handler before the first completes, orphaning the first handler's threading.Event.

Mitigation: The existing session lock (asyncio.Lock in routers/chat.py:64) should prevent this in normal operation, but consider logging a warning if a handler already exists for the session.


🟡 Important Findings

3. No Authentication on /api/chat/confirm Endpoint

File: src/gaia/ui/routers/chat.py:140-162

The confirm endpoint only validates that a handler exists and the confirm_id matches. A local attacker who guesses/sniffs the confirm_id (UUID v4) could confirm/deny on behalf of the user.

Context: This is acceptable for local-only deployments, but if Agent UI is ever exposed to a network (e.g., via tunnel), this becomes a vulnerability.

Recommendation: Add a comment documenting this security assumption, and consider adding a session token check for tunnel scenarios.

4. Hard-coded 60s Timeout Without Documentation

File: src/gaia/ui/sse_handler.py:576

deadline = time.time() + 60

The timeout is hard-coded in multiple places (sse_handler.py, ChatView.tsx) without configuration. Users may want to adjust this.

Recommendation: Extract to a constant at module level (e.g., TOOL_CONFIRM_TIMEOUT_SECONDS = 60) and document in the user guide.

5. localStorage "Always Allow" Not Scoped to Session

File: src/gaia/apps/webui/src/stores/notificationStore.ts:107-116

The gaia_always_allow_tools localStorage key persists globally across all sessions. If a user clicks "Always Allow" for run_shell_command in a trusted session, it auto-approves in ALL future sessions, including potentially malicious ones where an attacker controls the prompt.

Recommendation: Consider scoping to session ID, or add a UI to clear remembered permissions, or document this behavior prominently.

6. Missing Unit Tests for New Functionality

Grep results: No tests found for TOOLS_REQUIRING_CONFIRMATION, confirm_tool_execution, or SSEOutputHandler.resolve_confirmation.

Recommendation: Add tests for:

  • SSEOutputHandler.confirm_tool_execution() (timeout, cancellation, resolve)
  • POST /api/chat/confirm endpoint (valid confirm, expired confirm, wrong session)
  • Agent _execute_tool() guardrail integration

🟢 Minor Findings

7. Non-null Assertion on event.confirm_id

File: src/gaia/apps/webui/src/components/ChatView.tsx:614

api.confirmToolExecution(sessionId, event.confirm_id!, 'allow', false)

TypeScript non-null assertion (!) is used. If the backend ever sends a malformed event, this will throw.

Recommendation: Add defensive check:

if (!event.confirm_id) {
    console.error('[ChatView] tool_confirm event missing confirm_id');
    return;
}

8. Duplicate ALWAYS_ALLOW_KEY Definition

Files: ChatView.tsx:608, notificationStore.ts:108

The localStorage key gaia_always_allow_tools is defined in two places. Consider extracting to a shared constant.

9. Documentation Removed Warning Banner

File: docs/guides/agent-ui.mdx (lines 9-18 removed)

The "Tested Configuration" warning was removed. This is fine if the UI is now production-ready, but ensure this is intentional.


Architecture Consistency ✅

  • Follows existing OutputHandler pattern correctly
  • Reuses PermissionPrompt.tsx and notificationStore as designed
  • SSE event types properly extended
  • Handler lifecycle (register/unregister) is clean

Documentation 🟡

  • PR description is excellent
  • Missing: User-facing documentation about the confirmation popup feature
  • Missing: Documentation about "Always Allow" behavior and how to reset it

Suggested Next Steps

  1. Fix Critical Update installer and workflows/actions for CI/CD #1 - Use Literal["allow", "deny"] for action validation
  2. Add tests - At minimum, unit tests for the SSEOutputHandler confirmation flow
  3. Document the feature - Add a section to docs/guides/agent-ui.mdx explaining the confirmation popup
  4. Consider localStorage scoping - Either scope "Always Allow" per-session or document the global behavior

@itomek itomek added this pull request to the merge queue Mar 19, 2026
Merged via the queue into main with commit 3df90ff Mar 19, 2026
32 checks passed
@itomek itomek deleted the 438-tool-execution-guardrails-confirmation-popup-before-dangerous-commands branch March 19, 2026 17:46
kovtcharov and others added 4 commits March 19, 2026 17:08
- Rewrite agent-ui.mdx: remove redundant Get Started section (lives in
  quickstart), add "Search and Browse Files" + "Analyze Documents"
  capabilities, replace ASCII architecture with Mermaid diagram
- Update quickstart.mdx: task-focused intro (not "chatting"), simplify
  npm tab to published package, add auto-bootstrap note, remove
  redundant CLI tab and Desktop Installer content
- Move guides/agent-ui from Getting Started to Python Framework in
  docs.json navigation
- Add auto-bootstrap to gaia-ui.mjs: when Python backend not found,
  automatically runs install scripts (install.ps1/install.sh) to set
  up uv + Python 3.12 + amd-gaia into ~/.gaia/venv
- Add "gaia" bin entry to package.json alongside "gaia-ui"
- Guard against infinite spawn loop when npm "gaia" shadows Python CLI
- Update sdk/sdks/agent-ui.mdx: fix naming (Agent UI not GAIA Chat,
  Agent SDK not Chat SDK), update npm section to show gaia command
- Remove "Generative AI Is Awesome" from sdk/index.mdx
… GAIA personality

CLI:
- Add --ui, --cli, --ui-port top-level flags to gaia command
- Bare `gaia` defaults to launching Agent UI
- Interactive menu with [1] Agent UI [2] CLI [3] Help
- Extract _launch_agent_ui(), _launch_interactive_cli() helpers
- gaia chat --ui preserved as backward-compatible alias

npm package (gaia-ui):
- Auto-install Python backend on first run (uv + Python 3.12 + amd-gaia[ui])
- Pin Python package version to match npm package version
- Auto-update backend on version mismatch
- Run gaia init --profile minimal after first install
- --gaia-version flag for installing specific versions
- Clear error messages with manual install instructions at every step
- Remove "gaia" bin alias to avoid conflict with Python CLI

Agent personality:
- Rewrite ChatAgent system prompt for natural, direct personality
- GAIA speaks like a smart friend, not a corporate assistant
- Pushes back on wrong claims, avoids sycophancy
- No filler phrases, no "As an AI assistant" self-references

Documentation:
- Promote Agent UI to top-level under Python Framework in docs.json
- Rewrite guides/agent-ui.mdx as concise getting started page
- Update quickstart with npm install, update, uninstall, Node.js install instructions
- Add Top-Level Flags section to CLI reference
- Update deployment/ui.mdx with coming-soon warning for desktop installer
- Fix GaiaAgent -> ChatAgent in SDK docs
- Update index.mdx cards for Agent UI

Tests:
- Add chat concurrency, chat helpers, and utils helpers unit tests
- Update database tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Before this change, run_shell_command executed silently whenever the LLM
decided to use it — users had no visibility or consent. This adds a
blocking confirmation popup in the Agent UI before any shell command runs.

Backend changes:
- console.py: Add confirm_tool_execution() to OutputHandler (default
  auto-approves, preserving CLI behaviour)
- agent.py: Add TOOLS_REQUIRING_CONFIRMATION set and guardrail check in
  _execute_tool(); returns {"status":"denied"} if user declines
- sse_handler.py: SSEOutputHandler.confirm_tool_execution() emits a
  tool_confirm SSE event then blocks (0.5 s poll loop, 60 s timeout)
  until resolve_confirmation() is called or cancellation fires
- server.py: Add app.state.active_sse_handlers for per-session handler
  lookup by the confirm endpoint
- _chat_helpers.py: Register/unregister SSE handler around the stream;
  pass http_request through to enable the registry
- routers/chat.py: Add POST /api/chat/confirm endpoint that routes user
  Allow/Deny back to the blocked agent thread
- models.py: Add ToolConfirmRequest model

Frontend changes:
- types/index.ts: Add tool_confirm to StreamEventType; add confirm_id
  and timeout_seconds fields to StreamEvent
- api.ts: Add tool_confirm to AGENT_EVENT_TYPES; add
  confirmToolExecution() helper
- ChatView.tsx: Handle tool_confirm events — auto-approve if tool is in
  localStorage always-allow list, otherwise push a GaiaNotification to
  show the existing PermissionPrompt modal
- notificationStore.ts: HTTP fallback in respondToPermission() (routes
  to /api/chat/confirm when not in Electron); persist Always Allow in
  localStorage key gaia_always_allow_tools

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
itomek added a commit that referenced this pull request Mar 19, 2026
…SSE confirmation flow (#565)

Expand TOOLS_REQUIRING_CONFIRMATION from just run_shell_command to all
write/execute tools (write_file, edit_file, write_python_file, etc.).
Implement the full backend-to-frontend confirmation flow: SSEOutputHandler
now overrides confirm_tool_execution() to emit permission_request events
and block until the frontend responds via a new POST /api/chat/confirm-tool
endpoint. Frontend wires SSE events to the existing PermissionPrompt UI.
…defensive guards

- models.py: action field uses Literal["allow","deny"] — rejects invalid values via Pydantic
- _chat_helpers.py: log warning when SSE handler overwrites existing session registration
- routers/chat.py: add security comment on /api/chat/confirm (local-only assumption)
- sse_handler.py: extract TOOL_CONFIRM_TIMEOUT_SECONDS = 60 constant, use everywhere
- ChatView.tsx: add guard for missing confirm_id before non-null assertion
- notificationStore.ts: export ALWAYS_ALLOW_TOOLS_KEY constant (ChatView imports it)
- test_sse_confirmation.py: 12 new unit tests for confirmation timeout/allow/deny/endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
itomek added a commit that referenced this pull request Mar 23, 2026
PR #566 squash-merged a stale branch that had resolved merge conflicts by
keeping older file versions, reverting 3 previously-merged PRs from main:
- PR #564: TOCTOU upload locking security fix
- PR #565: Tool execution guardrails with confirmation popup
- PR #568: Agent UI overhaul (CSS design system, animations, UX polish)

Follow-up PRs #593/#604/#605 partially restored functionality. This PR
restores all remaining missing changes while preserving those follow-ups.

Changes:
- 24 files: clean restore from pre-revert commit (CSS, components, utils)
- Security: restore per-file asyncio.Lock upload guard (dependencies.py,
  documents.py, server.py)
- SSE handler: restore <think> block state machine, UUID-scoped confirms,
  timeout parameter, friendly error messages
- Frontend: restore AnimatedPresence, session hash badge, smooth streaming
  exit, custom model override UI, terminal typing animation, inference stats
- Backend: restore custom_model DB override, Lemonade stats fetching,
  friendlier user-facing error messages
- Tests: 497 passing, TypeScript build clean (1845 modules)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
itomek added a commit that referenced this pull request Mar 23, 2026
PR #566 squash-merged a stale branch that had resolved merge conflicts by
keeping older file versions, reverting 3 previously-merged PRs from main:
- PR #564: TOCTOU upload locking security fix
- PR #565: Tool execution guardrails with confirmation popup
- PR #568: Agent UI overhaul (CSS design system, animations, UX polish)

Follow-up PRs #593/#604/#605 partially restored functionality. This PR
restores all remaining missing changes while preserving those follow-ups.

Changes:
- 24 files: clean restore from pre-revert commit (CSS, components, utils)
- Security: restore per-file asyncio.Lock upload guard (dependencies.py,
  documents.py, server.py)
- SSE handler: restore <think> block state machine, UUID-scoped confirms,
  timeout parameter, friendly error messages
- Frontend: restore AnimatedPresence, session hash badge, smooth streaming
  exit, custom model override UI, terminal typing animation, inference stats
- Backend: restore custom_model DB override, Lemonade stats fetching,
  friendlier user-facing error messages
- Tests: 497 passing, TypeScript build clean (1845 modules)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
github-merge-queue bot pushed a commit that referenced this pull request Mar 23, 2026
… (#608)

## Summary

PR #566 was accidentally merged with stale conflict resolutions that
reverted 3 previously-merged PRs. Follow-up PRs #593/#604/#605 partially
restored functionality. This PR restores all remaining missing changes.

**Root cause:** During a `git merge origin/main` into the branch (commit
`f07b932`), conflict resolution kept the branch's older file versions,
discarding work from 3 PRs. The squash merge then propagated this to
main.

**Reverted PRs restored by this PR:**
- **#564** — TOCTOU race condition fix: per-file `asyncio.Lock` for
document uploads (`dependencies.py`, `routers/documents.py`,
`server.py`)
- **#565** — Tool execution guardrails: `<think>` block state machine,
UUID-scoped confirms, inference stats, custom model override, friendly
error messages (`sse_handler.py`, `_chat_helpers.py`, `models.py`)
- **#568** — Agent UI overhaul: CSS design system (glassmorphism,
animations), AnimatedPresence, session hash badge, smooth streaming
exit, terminal typing animation, custom model override UI,
`appendThinkingContent`, `format.ts` utilities (`App.tsx`,
`ChatView.tsx`, `AgentActivity.tsx`, `SettingsModal.tsx/css`,
`WelcomeScreen.tsx/css`, `Sidebar.tsx/css`, `MessageBubble.tsx/css`,
`chatStore.ts`, 12 other CSS files, `shell_tools.py`, `database.py`)

**Preserved follow-up PR additions:**
- #593: Device support banners, processor name display, Lemonade hints
- #604: `permission_request` events, `confirmTool` API, `fileList`
pass-through, PermissionPrompt
- #605: RAG indexing guards

## Test plan

- [x] `python -m pytest tests/unit/chat/ui/ --tb=short` — 497 passed
- [x] `python util/lint.py --black --isort` — all checks pass
- [x] `npm run build` in `src/gaia/apps/webui/` — 1,845 modules, no
TypeScript errors
- [ ] Smoke test: `gaia chat --ui` — verify UI loads, settings modal
shows custom model override, welcome screen has typing animation, chat
streams correctly
- [ ] Verify concurrent document uploads use per-file locking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent system changes audio Audio (ASR/TTS) changes chat Chat SDK changes cli CLI changes dependencies Dependency updates devops DevOps/infrastructure changes documentation Documentation changes electron Electron app changes eval Evaluation framework changes jira Jira agent changes llm LLM backend changes mcp MCP integration changes performance Performance-critical changes rag RAG system changes security Security-sensitive changes talk Talk agent changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tool execution guardrails: confirmation popup before dangerous commands

2 participants