# Tool Permission System Specification

**Status:** ✅ IMPLEMENTED (2026-01-20)

## Overview

This document specifies the approval and permission system for Kollabor CLI to control tool execution. The system allows the LLM to request operations (shell commands, file edits, MCP tools) that require user approval before execution.

**Implementation Complete:**
- Core permission system (manager, risk assessor, hook)
- Inline UI prompts in status/thinking area
- `/permissions` command for runtime configuration
- Event bus integration at SECURITY priority
- Session-scoped approvals
- Default mode: CONFIRM_ALL (all tools require approval)

## Table of Contents

1. [High-Level Architecture](#high-level-architecture)
2. [Approval Modes](#approval-modes)
3. [Tool Risk Assessment](#tool-risk-assessment)
4. [Permission Manager](#permission-manager)
5. [Confirmation Flow](#confirmation-flow)
6. [User Response Handling](#user-response-handling)
7. [Hook Integration](#hook-integration)
8. [Configuration](#configuration)
9. [Data Structures](#data-structures)
10. [UI Components](#ui-components)
11. [Security Features](#security-features)
12. [Implementation Files](#implementation-files)

---

## High-Level Architecture

```
┌─────────────────────────────────────────────────────────────────────┐
│                         Core Components                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                       │
│  ┌────────────────┐    ┌───────────────────┐    ┌────────────────┐  │
│  │ ToolExecutor   │────│ PermissionManager │────│    Config      │  │
│  └────────────────┘    └───────────────────┘    └────────────────┘  │
│          │                      │                                     │
│          │                      │                                     │
│          ▼                      ▼                                     │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │                    Risk Assessment Layer                     │    │
│  │  ┌──────────┐ ┌───────────┐ ┌─────────┐ ┌─────────────┐     │    │
│  │  │ Terminal │ │ FileWrite │ │   MCP   │ │ FileEdit    │     │    │
│  │  └──────────┘ └───────────┘ └─────────┘ └─────────────┘     │    │
│  └─────────────────────────────────────────────────────────────┘    │
│                                                                       │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │                    Event Bus (Hook System)                   │    │
│  │  Priority: SECURITY (900) - Permission checks                │    │
│  │  Events: TOOL_CALL_PRE, TOOL_PERMISSION_REQUEST              │    │
│  └─────────────────────────────────────────────────────────────┘    │
│                                                                       │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │                 Status Area (Inline Prompt)                  │    │
│  │  Renders in thinking/executing area using Box() style        │    │
│  │  Single keypress response: a/s/d/c (no arrow navigation)     │    │
│  └─────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘
```

### Key Components

1. **PermissionManager**: Central authority for permission decisions
2. **RiskAssessor**: Evaluates tool risk level based on patterns and config
3. **PermissionHook**: Event bus hook that intercepts tool execution
4. **PermissionStatusView**: Inline status area UI for user confirmation
5. **PermissionConfig**: Configuration for approval modes and rules

---

## Approval Modes

The system supports four distinct approval modes:

```python
from enum import Enum, auto

class ApprovalMode(Enum):
    """Approval mode for tool execution."""

    # Default: require confirmation for HIGH risk tools
    DEFAULT = auto()

    # Confirm all: require confirmation for all tool executions
    CONFIRM_ALL = auto()

    # Auto-approve: automatically approve file edit/write operations
    # Shell commands still require confirmation
    AUTO_APPROVE_EDITS = auto()

    # Trust all: automatically approve ALL tool executions (dangerous)
    TRUST_ALL = auto()
```

### Mode Behavior

| Mode | HIGH Risk | MEDIUM Risk | LOW Risk |
|------|-----------|-------------|----------|
| DEFAULT | Confirm | Auto-approve | Auto-approve |
| CONFIRM_ALL | Confirm | Confirm | Confirm |
| AUTO_APPROVE_EDITS | Confirm (shell) | Auto-approve | Auto-approve |
| TRUST_ALL | Auto-approve | Auto-approve | Auto-approve |

### Mode Selection

- **DEFAULT**: Active unless user changes via config or command
- **CONFIRM_ALL**: Activated via `/permissions strict` command
- **AUTO_APPROVE_EDITS**: Activated when user selects "Always approve edits"
- **TRUST_ALL**: Activated via `--trust-all` flag or env var (requires explicit opt-in)

---

## Tool Risk Assessment

### Risk Levels

```python
from enum import Enum, auto

class ToolRiskLevel(Enum):
    """Risk classification for tool operations."""

    # Always requires confirmation - destructive operations
    HIGH = auto()

    # Requires confirmation in strict mode - modifying operations
    MEDIUM = auto()

    # Never requires confirmation - read-only operations
    LOW = auto()

    # Unknown tool - treat as HIGH until assessed
    UNKNOWN = auto()
```

### Risk Assessment Rules

```python
from dataclasses import dataclass, field
from typing import List, Pattern
import re

@dataclass
class RiskAssessmentRules:
    """Rules for assessing tool risk levels."""

    # HIGH risk patterns (regex) - always block without confirmation
    high_risk_patterns: List[Pattern] = field(default_factory=lambda: [
        re.compile(r"rm\s+(-[rf]+\s+)*[/~]"),        # rm -rf /
        re.compile(r"rm\s+.*--no-preserve-root"),    # rm --no-preserve-root
        re.compile(r"mkfs\."),                        # mkfs operations
        re.compile(r"dd\s+.*of=/dev"),               # dd to device
        re.compile(r"chmod\s+(-R\s+)?777"),          # chmod 777
        re.compile(r">\s*/dev/sd[a-z]"),             # write to disk
        re.compile(r"curl.*\|\s*(ba)?sh"),           # curl | bash
        re.compile(r"wget.*\|\s*(ba)?sh"),           # wget | bash
        re.compile(r"sudo\s+rm"),                     # sudo rm
        re.compile(r":\(\)\s*\{"),                   # fork bomb
    ])

    # MEDIUM risk patterns - modifying operations
    medium_risk_patterns: List[Pattern] = field(default_factory=lambda: [
        re.compile(r"git\s+push"),                   # git push
        re.compile(r"git\s+reset\s+--hard"),         # git reset --hard
        re.compile(r"npm\s+publish"),                # npm publish
        re.compile(r"pip\s+install"),                # pip install
        re.compile(r"docker\s+build"),               # docker build
        re.compile(r"kubectl\s+apply"),              # kubectl apply
    ])

    # Tool types and their default risk levels
    tool_type_risks: dict = field(default_factory=lambda: {
        "terminal": ToolRiskLevel.MEDIUM,    # Shell commands
        "mcp": ToolRiskLevel.MEDIUM,         # MCP tools (unknown behavior)
        "file_write": ToolRiskLevel.MEDIUM,  # Writing files
        "file_edit": ToolRiskLevel.LOW,      # Editing existing files
        "file_read": ToolRiskLevel.LOW,      # Reading files
        "search": ToolRiskLevel.LOW,         # Search operations
    })

    # Trusted tool names (always LOW risk)
    trusted_tools: List[str] = field(default_factory=lambda: [
        "read_file",
        "list_directory",
        "search_file_content",
        "glob",
    ])

    # Blocked tools (always denied, no confirmation offered)
    blocked_tools: List[str] = field(default_factory=list)
```

### Risk Assessor Class

```python
from dataclasses import dataclass
from typing import Optional, Dict, Any
import logging

logger = logging.getLogger(__name__)

@dataclass
class RiskAssessmentResult:
    """Result of risk assessment for a tool."""

    level: ToolRiskLevel
    reason: str
    matched_pattern: Optional[str] = None
    tool_type: Optional[str] = None
    requires_confirmation: bool = False
    is_blocked: bool = False


class RiskAssessor:
    """Assesses risk level for tool operations."""

    def __init__(self, rules: RiskAssessmentRules, config: dict):
        self._rules = rules
        self._config = config

    def assess_tool(self, tool_data: Dict[str, Any]) -> RiskAssessmentResult:
        """
        Assess the risk level of a tool operation.

        Args:
            tool_data: Tool information from response parser
                - type: str (terminal, mcp, file_write, etc.)
                - name: str (tool name)
                - command: str (for terminal commands)
                - arguments: dict (tool arguments)

        Returns:
            RiskAssessmentResult with level and reasoning
        """
        tool_type = tool_data.get("type", "unknown")
        tool_name = tool_data.get("name", "")
        command = tool_data.get("command", "")

        # Check if tool is blocked
        if tool_name in self._rules.blocked_tools:
            return RiskAssessmentResult(
                level=ToolRiskLevel.HIGH,
                reason=f"Tool '{tool_name}' is blocked by configuration",
                is_blocked=True,
            )

        # Check if tool is trusted
        if tool_name in self._rules.trusted_tools:
            return RiskAssessmentResult(
                level=ToolRiskLevel.LOW,
                reason=f"Tool '{tool_name}' is in trusted list",
                tool_type=tool_type,
            )

        # For terminal commands, check patterns
        if tool_type == "terminal" and command:
            result = self._assess_command(command)
            if result:
                return result

        # Fall back to tool type default
        default_level = self._rules.tool_type_risks.get(
            tool_type, ToolRiskLevel.UNKNOWN
        )

        return RiskAssessmentResult(
            level=default_level,
            reason=f"Default risk for tool type '{tool_type}'",
            tool_type=tool_type,
        )

    def _assess_command(self, command: str) -> Optional[RiskAssessmentResult]:
        """Assess risk of a shell command."""
        # Check HIGH risk patterns
        for pattern in self._rules.high_risk_patterns:
            if pattern.search(command):
                return RiskAssessmentResult(
                    level=ToolRiskLevel.HIGH,
                    reason="Command matches high-risk pattern",
                    matched_pattern=pattern.pattern,
                    tool_type="terminal",
                    requires_confirmation=True,
                )

        # Check MEDIUM risk patterns
        for pattern in self._rules.medium_risk_patterns:
            if pattern.search(command):
                return RiskAssessmentResult(
                    level=ToolRiskLevel.MEDIUM,
                    reason="Command matches medium-risk pattern",
                    matched_pattern=pattern.pattern,
                    tool_type="terminal",
                )

        return None
```

---

## Permission Manager

The central authority for all permission decisions.

```python
from dataclasses import dataclass, field
from typing import Dict, Any, Optional, Set, Callable, Awaitable
from datetime import datetime, timedelta
import logging
import asyncio

logger = logging.getLogger(__name__)

@dataclass
class PermissionDecision:
    """Result of a permission check."""

    allowed: bool
    reason: str
    risk_level: ToolRiskLevel
    requires_user_confirmation: bool = False
    confirmation_details: Optional[Dict[str, Any]] = None
    timestamp: datetime = field(default_factory=datetime.now)


@dataclass
class ApprovalRecord:
    """Record of a user approval."""

    tool_type: str
    tool_name: str
    pattern: Optional[str]  # For "always approve" matching
    approved_at: datetime
    expires_at: Optional[datetime]  # None = session-scoped
    scope: str  # "once", "session", "always"


class PermissionManager:
    """
    Central manager for tool execution permissions.

    Integrates with event bus to intercept tool execution requests
    and enforce permission policies.
    """

    def __init__(
        self,
        config: dict,
        risk_assessor: RiskAssessor,
        event_bus: "EventBus",
    ):
        self._config = config
        self._risk_assessor = risk_assessor
        self._event_bus = event_bus

        # Session-scoped approvals
        self._session_approvals: Dict[str, ApprovalRecord] = {}

        # Approval mode (can change during session)
        self._approval_mode = self._load_approval_mode()

        # Pending confirmations (tool_id -> asyncio.Event)
        self._pending_confirmations: Dict[str, asyncio.Event] = {}
        self._confirmation_results: Dict[str, PermissionDecision] = {}

        # Callback for showing confirmation UI
        self._confirmation_callback: Optional[
            Callable[[Dict[str, Any]], Awaitable[PermissionDecision]]
        ] = None

        # Statistics
        self._stats = {
            "total_checks": 0,
            "auto_approved": 0,
            "user_approved": 0,
            "denied": 0,
            "blocked": 0,
        }

    def _load_approval_mode(self) -> ApprovalMode:
        """Load approval mode from config."""
        mode_str = self._config.get(
            "kollabor.permissions.approval_mode", "default"
        )
        mode_map = {
            "default": ApprovalMode.DEFAULT,
            "confirm_all": ApprovalMode.CONFIRM_ALL,
            "auto_approve_edits": ApprovalMode.AUTO_APPROVE_EDITS,
            "trust_all": ApprovalMode.TRUST_ALL,
        }
        return mode_map.get(mode_str, ApprovalMode.DEFAULT)

    @property
    def approval_mode(self) -> ApprovalMode:
        """Get current approval mode."""
        return self._approval_mode

    def set_approval_mode(self, mode: ApprovalMode) -> None:
        """
        Set approval mode for the session.

        Args:
            mode: New approval mode
        """
        logger.info(f"Approval mode changed: {self._approval_mode} -> {mode}")
        self._approval_mode = mode

    def set_confirmation_callback(
        self,
        callback: Callable[[Dict[str, Any]], Awaitable[PermissionDecision]],
    ) -> None:
        """
        Set callback for showing confirmation UI.

        Args:
            callback: Async function that shows confirmation and returns decision
        """
        self._confirmation_callback = callback

    async def check_permission(
        self,
        tool_data: Dict[str, Any],
    ) -> PermissionDecision:
        """
        Check if a tool execution is permitted.

        Args:
            tool_data: Tool information from response parser

        Returns:
            PermissionDecision with allowed status and reason
        """
        self._stats["total_checks"] += 1

        tool_id = tool_data.get("id", "unknown")
        tool_type = tool_data.get("type", "unknown")
        tool_name = tool_data.get("name", "")

        # Step 1: Risk assessment
        risk_result = self._risk_assessor.assess_tool(tool_data)

        # Step 2: Check if blocked
        if risk_result.is_blocked:
            self._stats["blocked"] += 1
            return PermissionDecision(
                allowed=False,
                reason=risk_result.reason,
                risk_level=risk_result.level,
            )

        # Step 3: Check approval mode
        if self._approval_mode == ApprovalMode.TRUST_ALL:
            self._stats["auto_approved"] += 1
            return PermissionDecision(
                allowed=True,
                reason="Trust all mode enabled",
                risk_level=risk_result.level,
            )

        # Step 4: Check session approvals
        approval_key = self._get_approval_key(tool_type, tool_name, tool_data)
        if approval_key in self._session_approvals:
            record = self._session_approvals[approval_key]
            if record.expires_at is None or record.expires_at > datetime.now():
                self._stats["auto_approved"] += 1
                return PermissionDecision(
                    allowed=True,
                    reason=f"Previously approved ({record.scope})",
                    risk_level=risk_result.level,
                )

        # Step 5: Determine if confirmation needed
        needs_confirmation = self._needs_confirmation(
            risk_result.level, tool_type
        )

        if not needs_confirmation:
            self._stats["auto_approved"] += 1
            return PermissionDecision(
                allowed=True,
                reason="Auto-approved based on risk level and mode",
                risk_level=risk_result.level,
            )

        # Step 6: Request user confirmation
        if self._confirmation_callback:
            confirmation_details = self._build_confirmation_details(
                tool_data, risk_result
            )
            decision = await self._confirmation_callback(confirmation_details)

            if decision.allowed:
                self._stats["user_approved"] += 1
                # Record approval if "always" scope
                if confirmation_details.get("approval_scope") == "session":
                    self._record_approval(tool_type, tool_name, tool_data, "session")
            else:
                self._stats["denied"] += 1

            return decision

        # No callback set - deny by default
        self._stats["denied"] += 1
        return PermissionDecision(
            allowed=False,
            reason="Confirmation required but no UI available",
            risk_level=risk_result.level,
            requires_user_confirmation=True,
        )

    def _needs_confirmation(
        self,
        risk_level: ToolRiskLevel,
        tool_type: str,
    ) -> bool:
        """Determine if confirmation is needed based on risk and mode."""
        if self._approval_mode == ApprovalMode.TRUST_ALL:
            return False

        if self._approval_mode == ApprovalMode.CONFIRM_ALL:
            return True

        if self._approval_mode == ApprovalMode.AUTO_APPROVE_EDITS:
            # Auto-approve file operations, confirm shell
            if tool_type in ("file_write", "file_edit"):
                return False
            if tool_type == "terminal":
                return True

        # DEFAULT mode: confirm HIGH risk only
        return risk_level in (ToolRiskLevel.HIGH, ToolRiskLevel.UNKNOWN)

    def _get_approval_key(
        self,
        tool_type: str,
        tool_name: str,
        tool_data: Dict[str, Any],
    ) -> str:
        """Generate key for approval lookup."""
        if tool_type == "terminal":
            # For shell commands, use root command as key
            command = tool_data.get("command", "")
            root_cmd = command.split()[0] if command else ""
            return f"terminal:{root_cmd}"
        return f"{tool_type}:{tool_name}"

    def _record_approval(
        self,
        tool_type: str,
        tool_name: str,
        tool_data: Dict[str, Any],
        scope: str,
    ) -> None:
        """Record an approval for future lookups."""
        key = self._get_approval_key(tool_type, tool_name, tool_data)
        self._session_approvals[key] = ApprovalRecord(
            tool_type=tool_type,
            tool_name=tool_name,
            pattern=None,
            approved_at=datetime.now(),
            expires_at=None if scope == "session" else datetime.now() + timedelta(hours=1),
            scope=scope,
        )

    def _build_confirmation_details(
        self,
        tool_data: Dict[str, Any],
        risk_result: RiskAssessmentResult,
    ) -> Dict[str, Any]:
        """Build details for confirmation UI."""
        tool_type = tool_data.get("type", "unknown")

        details = {
            "tool_id": tool_data.get("id"),
            "tool_type": tool_type,
            "tool_name": tool_data.get("name", ""),
            "risk_level": risk_result.level.name,
            "risk_reason": risk_result.reason,
            "matched_pattern": risk_result.matched_pattern,
        }

        if tool_type == "terminal":
            details["command"] = tool_data.get("command", "")
            details["root_command"] = details["command"].split()[0] if details["command"] else ""
        elif tool_type in ("file_write", "file_edit"):
            details["file_path"] = tool_data.get("arguments", {}).get("path", "")
            details["content_preview"] = self._get_content_preview(tool_data)
        elif tool_type == "mcp":
            details["server_name"] = tool_data.get("server_name", "")
            details["mcp_tool_name"] = tool_data.get("name", "")

        return details

    def _get_content_preview(
        self,
        tool_data: Dict[str, Any],
        max_lines: int = 10,
    ) -> str:
        """Get preview of file content for confirmation."""
        content = tool_data.get("arguments", {}).get("content", "")
        lines = content.split("\n")
        if len(lines) > max_lines:
            preview_lines = lines[:max_lines]
            preview_lines.append(f"... ({len(lines) - max_lines} more lines)")
            return "\n".join(preview_lines)
        return content

    def get_stats(self) -> Dict[str, int]:
        """Get permission check statistics."""
        return self._stats.copy()

    def clear_session_approvals(self) -> None:
        """Clear all session-scoped approvals."""
        self._session_approvals.clear()
        logger.info("Session approvals cleared")
```

---

## Confirmation Flow

### State Machine

```
┌──────────────┐
│   pending    │  <- Tool execution requested
└──────┬───────┘
       │
       ├──[blocked]────────────────→ ┌──────────┐
       │                              │  denied  │
       │                              └──────────┘
       ├──[auto-approved]
       │         │
       │         ▼
       │  ┌──────────────┐
       │  │  approved    │──────────→ Execute tool
       │  └──────────────┘
       │
       ├──[needs-confirmation]
       ▼
┌──────────────┐
│  confirming  │  <- Waiting for user
└──────┬───────┘
       │
       ├──[user-approved]──────────→ ┌──────────┐
       │                              │ approved │
       │                              └──────────┘
       ├──[user-denied]
       ▼
┌──────────────┐
│   denied     │  <- Terminal state
└──────────────┘
```

### Confirmation Types

```python
from enum import Enum, auto
from dataclasses import dataclass
from typing import Optional, List, Callable, Awaitable

class ConfirmationType(Enum):
    """Type of confirmation dialog to show."""

    # Shell command execution
    SHELL_COMMAND = auto()

    # File write (new file)
    FILE_WRITE = auto()

    # File edit (existing file)
    FILE_EDIT = auto()

    # MCP tool execution
    MCP_TOOL = auto()

    # Generic confirmation
    GENERIC = auto()


@dataclass
class ShellCommandConfirmation:
    """Confirmation details for shell commands."""

    type: ConfirmationType = ConfirmationType.SHELL_COMMAND
    command: str = ""
    root_command: str = ""
    working_directory: str = ""
    risk_level: str = ""
    risk_reason: str = ""
    on_confirm: Optional[Callable[[str], Awaitable[None]]] = None


@dataclass
class FileOperationConfirmation:
    """Confirmation details for file operations."""

    type: ConfirmationType = ConfirmationType.FILE_WRITE
    file_path: str = ""
    file_name: str = ""
    operation: str = ""  # "write", "edit", "delete"
    content_preview: str = ""
    diff_preview: Optional[str] = None
    original_content: Optional[str] = None
    new_content: str = ""
    risk_level: str = ""
    on_confirm: Optional[Callable[[str], Awaitable[None]]] = None


@dataclass
class MCPToolConfirmation:
    """Confirmation details for MCP tools."""

    type: ConfirmationType = ConfirmationType.MCP_TOOL
    server_name: str = ""
    tool_name: str = ""
    tool_display_name: str = ""
    arguments: dict = None
    risk_level: str = ""
    on_confirm: Optional[Callable[[str], Awaitable[None]]] = None
```

---

## User Response Handling

```python
from enum import Enum, auto

class ConfirmationResponse(Enum):
    """User response to confirmation dialog."""

    # Approve this execution once
    APPROVE_ONCE = auto()

    # Approve and remember for this session
    APPROVE_SESSION = auto()

    # Approve all similar operations (e.g., all file edits)
    APPROVE_ALWAYS = auto()

    # Approve this specific tool/server always (MCP)
    APPROVE_TOOL_ALWAYS = auto()

    # Deny this execution
    DENY = auto()

    # Cancel and abort the entire operation
    CANCEL = auto()


async def handle_confirmation_response(
    permission_manager: PermissionManager,
    tool_data: Dict[str, Any],
    response: ConfirmationResponse,
) -> PermissionDecision:
    """
    Handle user response to confirmation dialog.

    Args:
        permission_manager: Permission manager instance
        tool_data: Tool being confirmed
        response: User's response

    Returns:
        PermissionDecision reflecting the response
    """
    tool_type = tool_data.get("type", "unknown")
    tool_name = tool_data.get("name", "")
    risk_level = tool_data.get("risk_level", ToolRiskLevel.MEDIUM)

    if response == ConfirmationResponse.DENY:
        return PermissionDecision(
            allowed=False,
            reason="User denied execution",
            risk_level=risk_level,
        )

    if response == ConfirmationResponse.CANCEL:
        return PermissionDecision(
            allowed=False,
            reason="User cancelled operation",
            risk_level=risk_level,
        )

    if response == ConfirmationResponse.APPROVE_ONCE:
        return PermissionDecision(
            allowed=True,
            reason="User approved (once)",
            risk_level=risk_level,
        )

    if response == ConfirmationResponse.APPROVE_SESSION:
        permission_manager._record_approval(
            tool_type, tool_name, tool_data, "session"
        )
        return PermissionDecision(
            allowed=True,
            reason="User approved (session)",
            risk_level=risk_level,
        )

    if response == ConfirmationResponse.APPROVE_ALWAYS:
        # Switch to AUTO_APPROVE_EDITS mode for file operations
        if tool_type in ("file_write", "file_edit"):
            permission_manager.set_approval_mode(ApprovalMode.AUTO_APPROVE_EDITS)
        return PermissionDecision(
            allowed=True,
            reason="User approved (always)",
            risk_level=risk_level,
        )

    if response == ConfirmationResponse.APPROVE_TOOL_ALWAYS:
        # For MCP: whitelist this specific tool
        permission_manager._record_approval(
            tool_type, tool_name, tool_data, "session"
        )
        return PermissionDecision(
            allowed=True,
            reason=f"User approved tool '{tool_name}' always",
            risk_level=risk_level,
        )

    # Default: deny
    return PermissionDecision(
        allowed=False,
        reason="Unknown response",
        risk_level=risk_level,
    )
```

---

## Hook Integration

### Permission Hook

```python
from kollabor.events.models import EventType, HookPriority
from typing import Dict, Any
import logging

logger = logging.getLogger(__name__)


class PermissionHook:
    """
    Event bus hook for enforcing tool permissions.

    Registers at SECURITY priority (900) to intercept tool execution
    before it happens.
    """

    def __init__(self, permission_manager: PermissionManager):
        self._permission_manager = permission_manager

    def register(self, event_bus: "EventBus") -> None:
        """Register hooks with event bus."""
        event_bus.register_hook(
            event_type=EventType.TOOL_CALL_PRE,
            handler=self._handle_tool_pre,
            priority=HookPriority.SECURITY,
            source="permission_system",
        )

    async def _handle_tool_pre(
        self,
        event: "Event",
        data: Dict[str, Any],
    ) -> Dict[str, Any]:
        """
        Handle TOOL_CALL_PRE event.

        Checks permission before tool execution. If denied,
        sets event.cancelled = True to prevent execution.

        Args:
            event: Event object
            data: Event data containing tool_data

        Returns:
            Modified data dict
        """
        tool_data = data.get("tool_data", {})

        if not tool_data:
            logger.warning("TOOL_CALL_PRE received without tool_data")
            return data

        # Check permission
        decision = await self._permission_manager.check_permission(tool_data)

        # Add decision to data for downstream hooks
        data["permission_decision"] = {
            "allowed": decision.allowed,
            "reason": decision.reason,
            "risk_level": decision.risk_level.name,
        }

        if not decision.allowed:
            # Cancel the event to prevent tool execution
            event.cancelled = True
            event.cancel_reason = decision.reason

            logger.info(
                f"Tool execution denied: {tool_data.get('name', 'unknown')} - "
                f"{decision.reason}"
            )
        else:
            logger.debug(
                f"Tool execution approved: {tool_data.get('name', 'unknown')} - "
                f"{decision.reason}"
            )

        return data
```

### New Event Types

```python
# Add to kollabor/events/models.py EventType enum

class EventType(Enum):
    # ... existing events ...

    # Permission events
    PERMISSION_CHECK = "permission_check"           # Permission being checked
    PERMISSION_GRANTED = "permission_granted"       # Permission granted
    PERMISSION_DENIED = "permission_denied"         # Permission denied
    PERMISSION_CONFIRMATION = "permission_confirmation"  # Confirmation requested
```

---

## Configuration

### Config Schema

```python
# Permission configuration defaults
PERMISSION_CONFIG_DEFAULTS = {
    "kollabor.permissions": {
        # Master switch
        "enabled": True,

        # Default approval mode
        "approval_mode": "default",  # default, confirm_all, auto_approve_edits, trust_all

        # Audit logging
        "audit_log_enabled": True,
        "audit_log_path": "~/.kollabor-cli/logs/permissions.log",

        # Risk assessment
        "risk_assessment": {
            # Custom high-risk patterns (regex)
            "high_risk_patterns": [],

            # Custom medium-risk patterns (regex)
            "medium_risk_patterns": [],

            # Trusted tools (always auto-approve)
            "trusted_tools": [
                "read_file",
                "list_directory",
                "search_file_content",
                "glob",
            ],

            # Blocked tools (always deny)
            "blocked_tools": [],

            # Trust all MCP servers by default
            "trust_mcp_servers": False,

            # Trusted MCP server names
            "trusted_mcp_servers": [],
        },

        # UI settings
        "ui": {
            # Show risk level in confirmation
            "show_risk_level": True,

            # Show matched pattern in confirmation
            "show_matched_pattern": True,

            # Confirmation timeout (seconds, 0 = no timeout)
            "confirmation_timeout": 0,

            # Default response on timeout
            "timeout_response": "deny",
        },
    },
}
```

### Environment Variables

```bash
# Override approval mode
KOLLABOR_PERMISSION_MODE=trust_all

# Disable permission system entirely
KOLLABOR_PERMISSIONS_ENABLED=false

# Quick trust-all for automation
KOLLABOR_TRUST_ALL=true
```

---

## Data Structures

### Tool Data (from ResponseParser)

```python
@dataclass
class ToolData:
    """Tool data extracted from LLM response."""

    id: str                        # Unique identifier for this tool call
    type: str                      # terminal, mcp, file_write, file_edit, etc.
    name: str                      # Tool name
    command: Optional[str]         # For terminal commands
    arguments: Dict[str, Any]      # Tool arguments
    server_name: Optional[str]     # For MCP tools
    raw_response: Dict[str, Any]   # Original response data
```

### Permission Audit Log Entry

```python
@dataclass
class PermissionAuditEntry:
    """Audit log entry for permission decisions."""

    timestamp: datetime
    tool_id: str
    tool_type: str
    tool_name: str
    command: Optional[str]
    risk_level: str
    decision: str                  # approved, denied, blocked
    reason: str
    approval_mode: str
    user_response: Optional[str]   # If user was prompted
    session_id: str
```

---

## UI Components

### Status Area Permission Prompt

The permission prompt renders inline in the status/activity area where "Thinking..." and
"Executing..." normally appear. This keeps the UI non-intrusive and consistent with
existing patterns.

#### Visual Design

Uses the design system Box() style with solid blocks (no box-drawing characters):

```
Normal execution (no confirmation needed):

  ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
     Executing: grep -r "TODO" src/
  ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀


With permission required (HIGH risk shell command):

  ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
     permission required                                      HIGH RISK
     Bash(rm -rf ./node_modules)
     a approve   s session   p this cmd   d deny   c cancel
  ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀


With permission required (file edit):

  ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
     permission required                                    MEDIUM RISK
     Edit(src/config.py)
     a approve   s session   p project   A always edits   d deny
  ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀


With permission required (MCP tool):

  ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
     permission required                                    MEDIUM RISK
     MCP(puppeteer/puppeteer_navigate)
     a approve   s session   p project   t trust tool   d deny
  ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
```

Tool format: `Bash(cmd)`, `Edit(path)`, `Write(path)`, `Read(path)`, `MCP(server/tool)`

#### Keyboard Shortcuts Reference

**Always available:**
| Key | Action | Description |
|-----|--------|-------------|
| `a` | Approve once | Allow this single execution |
| `s` | Session | Remember approval for this session |
| `p` | Project | Approve for project (label varies - see below) |
| `d` | Deny | Block this execution |
| `ESC` | Cancel | Same as deny |

**Context-specific:**
| Key | Context | Description |
|-----|---------|-------------|
| `A` | File write/edit | Switch to AUTO_APPROVE_EDITS mode |
| `t` | MCP tools | Whitelist this specific tool |
| `c` | Terminal commands | Cancel entire operation |

**Project approval labels:**
- `file_read` → "all reads" (gitignored files still protected)
- `terminal` → "this cmd"
- other → "project"

**Known limitation:** No timeout on permission prompt. If stuck, press `d` or `ESC`.

#### Key Design Principles

1. **No button borders** - Options are plain text with key prefix
2. **Box() style** - Uses solid block characters (▄▀) not box-drawing (╭╮╰╯│─)
3. **Inline in status area** - Replaces "Executing..." during confirmation wait
4. **Single keypress** - No arrow navigation, just press a/s/d/c directly
5. **Risk level colored** - HIGH=red, MEDIUM=yellow, LOW=green

### Permission Status View

```python
from kollabor.ui.design_system import T, S, solid, solid_fg, Box
from kollabor.io.terminal_state import get_terminal_width
from typing import Optional, Dict, Any, Callable
import logging

logger = logging.getLogger(__name__)


class PermissionStatusView:
    """
    Status view for permission confirmation.

    Renders in the status/activity area where "Thinking..." and
    "Executing..." normally appear. Uses Box() style rendering.
    """

    def __init__(
        self,
        confirmation_details: Dict[str, Any],
        on_response: Callable[[ConfirmationResponse], None],
    ):
        self._details = confirmation_details
        self._on_response = on_response
        self._options = self._build_options()

    def _build_options(self) -> list:
        """Build list of response options based on tool type."""
        tool_type = self._details.get("tool_type", "unknown")

        options = [
            ("a", "approve", ConfirmationResponse.APPROVE_ONCE),
            ("s", "session", ConfirmationResponse.APPROVE_SESSION),
        ]

        if tool_type in ("file_write", "file_edit"):
            options.append(
                ("A", "always edits", ConfirmationResponse.APPROVE_ALWAYS)
            )
        elif tool_type == "mcp":
            options.append(
                ("t", "trust tool", ConfirmationResponse.APPROVE_TOOL_ALWAYS)
            )

        options.append(("d", "deny", ConfirmationResponse.DENY))

        # Only add cancel for shell commands (more dangerous)
        if tool_type == "terminal":
            options.append(("c", "cancel", ConfirmationResponse.CANCEL))

        return options

    def render(self) -> list:
        """
        Render the permission prompt for the status area.

        Returns:
            List of formatted lines to display
        """
        width = get_terminal_width()
        lines = []

        # Top edge
        lines.append(solid_fg("▄" * width, T().dark[0]))

        # Header line: "permission required" + risk level
        risk_level = self._details.get("risk_level", "UNKNOWN")
        risk_text = f"{risk_level} RISK"
        header = "permission required"
        padding = width - len(header) - len(risk_text) - 6  # 6 for margins
        header_line = f"   {header}{' ' * padding}{risk_text}   "
        risk_color = self._get_risk_color(risk_level)
        lines.append(solid(header_line, T().dark[0], risk_color, width))

        # Tool description line
        tool_line = self._get_tool_description()
        lines.append(solid(f"   {tool_line:<{width-6}}   ", T().dark[0], T().text, width))

        # Options line (no borders, plain text)
        options_text = "   " + "   ".join(
            f"{key} {label}" for key, label, _ in self._options
        )
        lines.append(solid(f"{options_text:<{width}}", T().dark[0], T().text_dim, width))

        # Bottom edge
        lines.append(solid_fg("▀" * width, T().dark[0]))

        return lines

    def _get_tool_description(self) -> str:
        """Get single-line tool description."""
        tool_type = self._details.get("tool_type", "unknown")
        width = get_terminal_width()

        if tool_type == "terminal":
            command = self._details.get("command", "")
            max_len = width - 20
            if len(command) > max_len:
                command = command[:max_len - 3] + "..."
            return f"Bash({command})"

        elif tool_type == "file_write":
            file_path = self._details.get("file_path", "")
            max_len = width - 20
            if len(file_path) > max_len:
                file_path = "..." + file_path[-(max_len - 3):]
            return f"Write({file_path})"

        elif tool_type == "file_edit":
            file_path = self._details.get("file_path", "")
            max_len = width - 20
            if len(file_path) > max_len:
                file_path = "..." + file_path[-(max_len - 3):]
            return f"Edit({file_path})"

        elif tool_type in ("mcp_tool", "mcp"):
            server = self._details.get("server_name", "")
            tool = self._details.get("mcp_tool_name", "")
            if not server or not tool:
                return f"MCP({tool or server or 'unknown'})"
            return f"MCP({server}/{tool})"

        return f"Tool({self._details.get('tool_name', 'unknown')})"

    def _get_risk_color(self, risk_level: str) -> tuple:
        """Get color tuple for risk level."""
        colors = {
            "HIGH": T().error,
            "MEDIUM": T().warning,
            "LOW": T().success,
            "UNKNOWN": T().text_dim,
        }
        return colors.get(risk_level, T().text)

    def handle_keypress(self, key_char: str) -> bool:
        """
        Handle single keypress for option selection.

        Args:
            key_char: The character pressed

        Returns:
            True if keypress was handled, False otherwise
        """
        for key, _, response in self._options:
            if key_char == key:
                self._on_response(response)
                return True

        # Escape always cancels
        if key_char == '\x1b':  # ESC
            self._on_response(ConfirmationResponse.CANCEL)
            return True

        return False

    @property
    def options(self) -> list:
        """Get available options."""
        return self._options
```

### Integration with Layout System

The permission view integrates with the existing layout/status area system:

```python
# In kollabor/io/layout.py or similar

class LayoutManager:
    """Manages terminal layout including status area."""

    def __init__(self, ...):
        # ... existing init ...
        self._permission_view: Optional[PermissionStatusView] = None
        self._permission_response_event: Optional[asyncio.Event] = None
        self._permission_response: Optional[ConfirmationResponse] = None

    async def show_permission_prompt(
        self,
        confirmation_details: Dict[str, Any],
    ) -> ConfirmationResponse:
        """
        Show permission prompt in status area and wait for response.

        Args:
            confirmation_details: Details about the tool requiring permission

        Returns:
            User's confirmation response
        """
        self._permission_response_event = asyncio.Event()
        self._permission_response = None

        def on_response(response: ConfirmationResponse) -> None:
            self._permission_response = response
            self._permission_response_event.set()

        self._permission_view = PermissionStatusView(
            confirmation_details=confirmation_details,
            on_response=on_response,
        )

        # Trigger re-render of status area
        self._render_status_area()

        # Wait for user response
        await self._permission_response_event.wait()

        # Clear permission view
        self._permission_view = None
        self._render_status_area()

        return self._permission_response

    def _render_status_area(self) -> None:
        """Render the status area content."""
        if self._permission_view:
            # Render permission prompt
            lines = self._permission_view.render()
            self._write_status_lines(lines)
        elif self._is_thinking:
            # Render thinking animation
            self._render_thinking()
        elif self._is_executing:
            # Render execution status
            self._render_executing()
        else:
            # Clear status area
            self._clear_status_area()

    def handle_status_keypress(self, key_char: str) -> bool:
        """
        Handle keypress when status area is active.

        Args:
            key_char: Character pressed

        Returns:
            True if handled by permission view
        """
        if self._permission_view:
            return self._permission_view.handle_keypress(key_char)
        return False
```

### Input Handler Integration

The input handler routes keypresses to the permission view when active:

```python
# In kollabor/io/input_handler.py

class InputHandler:
    """Handles terminal input."""

    async def _process_keypress(self, key_data: dict) -> None:
        """Process a keypress event."""
        key_char = key_data.get("char", "")

        # Check if permission prompt is active
        if self._layout_manager.has_active_permission_prompt():
            if self._layout_manager.handle_status_keypress(key_char):
                return  # Handled by permission view

        # ... existing keypress handling ...
```

### Flow Diagram

```
LLM requests tool execution
         |
         v
  ┌──────────────────┐
  │ PermissionManager│
  │  check_permission│
  └────────┬─────────┘
           |
      [needs confirmation]
           |
           v
  ┌──────────────────┐
  │  LayoutManager   │
  │show_permission_  │
  │      prompt()    │
  └────────┬─────────┘
           |
           v
  ┌──────────────────┐
  │ Status Area      │  <-- User sees Box() styled prompt
  │ PermissionStatus │
  │      View        │
  └────────┬─────────┘
           |
      [user presses key]
           |
           v
  ┌──────────────────┐
  │  InputHandler    │
  │ routes to        │
  │ permission view  │
  └────────┬─────────┘
           |
           v
  ┌──────────────────┐
  │ on_response()    │
  │ callback fired   │
  └────────┬─────────┘
           |
           v
  ┌──────────────────┐
  │ asyncio.Event    │
  │     set()        │
  └────────┬─────────┘
           |
           v
  ┌──────────────────┐
  │ Permission       │
  │ decision returned│
  └────────┬─────────┘
           |
      [if approved]
           |
           v
  ┌──────────────────┐
  │ Tool executes    │
  │ Status shows     │
  │ "Executing..."   │
  └──────────────────┘
```

---

## Security Features

### 1. Path Validation

```python
from pathlib import Path
from typing import Optional

def validate_file_path(
    path: str,
    project_root: Path,
    allow_outside: bool = False,
) -> tuple[bool, Optional[str]]:
    """
    Validate that a file path is safe.

    Args:
        path: Path to validate
        project_root: Project root directory
        allow_outside: Whether to allow paths outside project

    Returns:
        Tuple of (is_valid, error_message)
    """
    try:
        resolved = Path(path).resolve()

        # Must be absolute
        if not resolved.is_absolute():
            return False, "Path must be absolute"

        # Check if within project root
        if not allow_outside:
            try:
                resolved.relative_to(project_root)
            except ValueError:
                return False, f"Path must be within project root: {project_root}"

        # Check for suspicious patterns
        path_str = str(resolved)
        suspicious_patterns = [
            "/etc/passwd",
            "/etc/shadow",
            "/.ssh/",
            "/.aws/",
            "/.env",
        ]
        for pattern in suspicious_patterns:
            if pattern in path_str:
                return False, f"Path contains suspicious pattern: {pattern}"

        return True, None

    except Exception as e:
        return False, f"Path validation error: {e}"
```

### 2. Command Sanitization

```python
import shlex
from typing import Optional

def sanitize_command(command: str) -> tuple[bool, Optional[str], Optional[str]]:
    """
    Sanitize and validate a shell command.

    Args:
        command: Command to sanitize

    Returns:
        Tuple of (is_safe, sanitized_command, error_message)
    """
    # Block command substitution
    if "$(" in command or "`" in command:
        return False, None, "Command substitution not allowed"

    # Block dangerous redirections
    dangerous_redirects = [
        "> /dev/sd",
        ">> /dev/sd",
        "> /etc/",
        ">> /etc/",
    ]
    for redirect in dangerous_redirects:
        if redirect in command:
            return False, None, f"Dangerous redirection detected: {redirect}"

    try:
        # Parse command to extract components
        parts = shlex.split(command)
        if not parts:
            return False, None, "Empty command"

        return True, command, None

    except ValueError as e:
        return False, None, f"Command parsing error: {e}"
```

### 3. Audit Logging

```python
import json
from pathlib import Path
from datetime import datetime
import logging

logger = logging.getLogger(__name__)


class PermissionAuditLogger:
    """Audit logger for permission decisions."""

    def __init__(self, log_path: Path, enabled: bool = True):
        self._log_path = log_path
        self._enabled = enabled
        self._session_id = datetime.now().strftime("%Y%m%d_%H%M%S")

        if self._enabled:
            self._log_path.parent.mkdir(parents=True, exist_ok=True)

    def log_decision(
        self,
        tool_data: Dict[str, Any],
        decision: PermissionDecision,
        user_response: Optional[str] = None,
    ) -> None:
        """Log a permission decision."""
        if not self._enabled:
            return

        entry = PermissionAuditEntry(
            timestamp=datetime.now(),
            tool_id=tool_data.get("id", "unknown"),
            tool_type=tool_data.get("type", "unknown"),
            tool_name=tool_data.get("name", ""),
            command=tool_data.get("command"),
            risk_level=decision.risk_level.name,
            decision="approved" if decision.allowed else "denied",
            reason=decision.reason,
            approval_mode=str(decision),
            user_response=user_response,
            session_id=self._session_id,
        )

        try:
            with open(self._log_path, "a") as f:
                f.write(json.dumps(entry.__dict__, default=str) + "\n")
        except Exception as e:
            logger.error(f"Failed to write audit log: {e}")
```

---

## Implementation Files

### New Files to Create

```
kollabor/llm/permissions/
├── __init__.py
├── manager.py              # PermissionManager class
├── risk_assessor.py        # RiskAssessor class
├── hook.py                 # PermissionHook class
├── models.py               # Data models (enums, dataclasses)
├── audit.py                # PermissionAuditLogger
└── config.py               # Permission config defaults

kollabor/io/status/
├── permission_status_view.py  # PermissionStatusView class

kollabor/utils/
├── security.py             # Path validation, command sanitization
```

### Files to Modify

```
kollabor/llm/tool_executor.py
    - Import and initialize PermissionManager
    - Check permission before execute_tool()
    - Handle denied permissions gracefully

kollabor/io/layout.py
    - Add show_permission_prompt() method
    - Add _permission_view state
    - Integrate with _render_status_area()

kollabor/io/input_handler.py
    - Route keypresses to permission view when active

kollabor/events/models.py
    - Add new EventType values for permissions

kollabor/config/loader.py
    - Add permission config defaults

kollabor/application.py
    - Initialize PermissionManager
    - Wire up status area confirmation callback
    - Register PermissionHook with event bus
```

---

## Integration Example

### In tool_executor.py

```python
from kollabor.llm.permissions import PermissionManager, PermissionHook

class ToolExecutor:
    def __init__(
        self,
        event_bus: EventBus,
        config: dict,
        # ... other params
    ):
        # ... existing init ...

        # Initialize permission system
        self._permission_manager = PermissionManager(
            config=config,
            risk_assessor=RiskAssessor(
                rules=RiskAssessmentRules(),
                config=config,
            ),
            event_bus=event_bus,
        )

        # Register permission hook
        permission_hook = PermissionHook(self._permission_manager)
        permission_hook.register(event_bus)

    async def execute_tool(self, tool_data: Dict[str, Any]) -> Dict[str, Any]:
        """Execute a tool with permission checking."""

        # Permission check happens via TOOL_CALL_PRE hook
        # If denied, event.cancelled will be True

        event = await self._event_bus.emit_with_hooks(
            EventType.TOOL_CALL_PRE,
            {"tool_data": tool_data},
            "tool_executor",
        )

        if event.cancelled:
            return {
                "success": False,
                "error": f"Permission denied: {event.cancel_reason}",
                "permission_denied": True,
            }

        # Proceed with execution
        # ... existing execution code ...
```

### In application.py

```python
async def _initialize_permission_system(self) -> None:
    """Initialize the permission system with status area UI."""

    async def show_permission_prompt(
        details: Dict[str, Any]
    ) -> PermissionDecision:
        """Show permission prompt in status area and wait for response."""
        # Use layout manager to show inline permission prompt
        response = await self._layout_manager.show_permission_prompt(details)

        return await handle_confirmation_response(
            self._permission_manager,
            details,
            response,
        )

    self._permission_manager.set_confirmation_callback(show_permission_prompt)
```

### In layout.py

```python
from kollabor.io.status.permission_status_view import PermissionStatusView
from kollabor.llm.permissions.models import ConfirmationResponse

class LayoutManager:
    """Manages terminal layout including status area."""

    def __init__(self, ...):
        # ... existing init ...
        self._permission_view: Optional[PermissionStatusView] = None
        self._permission_response_event: Optional[asyncio.Event] = None
        self._permission_response: Optional[ConfirmationResponse] = None

    def has_active_permission_prompt(self) -> bool:
        """Check if permission prompt is currently displayed."""
        return self._permission_view is not None

    async def show_permission_prompt(
        self,
        confirmation_details: Dict[str, Any],
    ) -> ConfirmationResponse:
        """
        Show permission prompt in status area and wait for response.

        Replaces the normal "Thinking..." or "Executing..." status with
        the permission prompt. Uses Box() style rendering.
        """
        self._permission_response_event = asyncio.Event()
        self._permission_response = None

        def on_response(response: ConfirmationResponse) -> None:
            self._permission_response = response
            self._permission_response_event.set()

        self._permission_view = PermissionStatusView(
            confirmation_details=confirmation_details,
            on_response=on_response,
        )

        # Trigger re-render of status area with permission prompt
        self._render_status_area()

        # Wait for user keypress
        await self._permission_response_event.wait()

        # Clear permission view, restore normal status area
        self._permission_view = None
        self._render_status_area()

        return self._permission_response

    def handle_status_keypress(self, key_char: str) -> bool:
        """Route keypress to permission view if active."""
        if self._permission_view:
            return self._permission_view.handle_keypress(key_char)
        return False
```

---

## Summary

The permission system provides:

1. **Flexible approval modes** (default, confirm_all, auto_approve_edits, trust_all)
2. **Risk-based assessment** with configurable patterns
3. **Session-scoped approvals** for user convenience
4. **Event bus integration** at SECURITY priority
5. **Audit logging** for compliance and debugging
6. **Inline status area UI** using Box() style rendering
7. **Single keypress interaction** (a/s/d/c) - no arrow navigation needed
8. **Security features** including path validation and command sanitization

### UI Design Principles

- Renders in status area (where "Thinking..." and "Executing..." appear)
- Uses Box() style with solid blocks (▄▀) - no box-drawing characters
- No borders on buttons - plain text with key prefix
- Risk level color-coded: HIGH=red, MEDIUM=yellow, LOW=green
- Non-blocking to chat flow - replaces status, doesn't interrupt

The implementation follows Kollabor CLI's existing patterns:
- snake_case function names
- Async/await throughout
- Event bus hook system
- Configuration via dot notation
- Design system (T(), solid(), solid_fg())
- Terminal state via get_terminal_width()
