# Vulnerability Verification Report

## 1. Summary
- **Vulnerability Type**: Business Logic (CWE-841) — Improper enforcement of behavioral workflow
- **Flagged Location**: `src/praisonai/praisonai/ui/chat.py:159` and `src/praisonai/praisonai/ui/code.py:158`
- **Vulnerability Description**: Both Chainlit UI modules (`chat.py` and `code.py`) hardcode `config.approval_mode = "auto"` after loading administrator configuration from the `PRAISON_APPROVAL_MODE` environment variable, silently overriding any `"manual"` or `"scoped"` approval setting. This defeats the human-in-the-loop approval gate for all ACP tool executions, including shell command execution via `subprocess.run(..., shell=True)`.

## 2. Analysis Logic

### Step 1: Confirming the hardcoded override in `chat.py`

Read `src/praisonai/praisonai/ui/chat.py:151-163` — the `_get_interactive_tools()` function:

```python
def _get_interactive_tools():
    """Lazy load interactive tools (ACP, LSP, basic) with trust mode."""
    if 'interactive_tools' not in _cached_modules:
        try:
            _profile_start("load_interactive_tools")
            from praisonai.cli.features.interactive_tools import get_interactive_tools, ToolConfig
            config = ToolConfig.from_env()            # line 157: reads PRAISON_APPROVAL_MODE
            config.workspace = os.getcwd()
            config.approval_mode = "auto"            # line 159: HARDCODED OVERRIDE
            tools = get_interactive_tools(config=config)
            ...
```

The same pattern exists in `src/praisonai/praisonai/ui/code.py:156-158`:

```python
config = ToolConfig.from_env()
config.workspace = os.environ.get("PRAISONAI_CODE_REPO_PATH", os.getcwd())
config.approval_mode = "auto"  # Trust mode - auto-approve all tool executions
```

**Analysis**: Line 157 calls `ToolConfig.from_env()` which properly reads the `PRAISON_APPROVAL_MODE` environment variable, but line 159 unconditionally overwrites it to `"auto"`. Any administrator who set `PRAISON_APPROVAL_MODE=manual` or `=scoped` has their configuration silently discarded.

### Step 2: Verifying `ToolConfig.from_env()` correctly supports the env variable

Read `src/praisonai/praisonai/cli/features/interactive_tools.py:68-105`:

```python
@dataclass
class ToolConfig:
    """Configuration for tool loading."""
    workspace: str = field(default_factory=os.getcwd)
    enable_acp: bool = True
    enable_lsp: bool = True
    enable_basic: bool = True
    approval_mode: str = "auto"  # auto, manual, scoped     # line 75: default "auto"
    ...
    
    @classmethod
    def from_env(cls) -> "ToolConfig":
        config = cls()
        ...
        # Check approval mode
        approval = os.environ.get("PRAISON_APPROVAL_MODE", "")
        if approval in ("auto", "manual", "scoped"):
            config.approval_mode = approval                  # line 103: sets from env
        return config
```

**Analysis**: `from_env()` at line 101-103 correctly reads `PRAISON_APPROVAL_MODE` and sets `config.approval_mode` to the administrator's chosen value if valid. The class default (line 75) is `"auto"`, but this is meant to be overridable via the env var. When the admin sets `PRAISON_APPROVAL_MODE=manual`, `from_env()` returns `config.approval_mode = "manual"`. However, `chat.py:159` and `code.py:158` immediately overwrite this back to `"auto"`.

### Step 3: Tracing approval_mode into the RuntimeConfig

Read `src/praisonai/praisonai/cli/features/interactive_tools.py:196-204` — `_load_acp_tools()`:

```python
runtime_config = RuntimeConfig(
    workspace=config.workspace,
    lsp_enabled=config.lsp_enabled,
    acp_enabled=config.acp_enabled,
    approval_mode=config.approval_mode,     # line 202: flows config.approval_mode
)
```

Read `src/praisonai/praisonai/cli/features/interactive_runtime.py:38-43` — `RuntimeConfig`:

```python
@dataclass
class RuntimeConfig:
    """Configuration for InteractiveRuntime."""
    workspace: str = "."
    lsp_enabled: bool = True
    acp_enabled: bool = True
    approval_mode: str = "manual"  # manual, auto, scoped   # line 43: default "manual"
```

**Analysis**: `RuntimeConfig` defaults to `"manual"` (line 43) — a safer default — while `ToolConfig` defaults to `"auto"` (line 75). The `ToolConfig` value (overridden to `"auto"` by chat.py/code.py) is passed into `RuntimeConfig`, overriding its safer default.

### Step 4: How approval_mode controls command execution in `agent_tools.py`

Read `src/praisonai/praisonai/cli/features/agent_tools.py:225-277` — `acp_execute_command()`:

```python
def acp_execute_command(command: str, cwd: str = None) -> str:
    ...
    async def _execute():
        prompt = f"run command: {command}"
        result = await orchestrator.create_plan(prompt)
        ...
        plan = result.plan
        
        # Commands require approval
        auto_approve = runtime.config.approval_mode == "auto"    # line 249: always True
        approved = await orchestrator.approve_plan(plan, auto=auto_approve)
        
        if not approved:
            return json.dumps({
                "success": False,
                "error": "Command requires approval",
                "requires_approval": True
            })
        
        result = await orchestrator.apply_plan(plan)             # line 259: proceeds
```

**Analysis**: Line 249 checks `runtime.config.approval_mode == "auto"`. Since `config.approval_mode` was hardcoded to `"auto"` in chat.py:159 / code.py:158, `auto_approve` is always `True`. The plan is always approved and `apply_plan()` is called unconditionally.

### Step 5: The `approve_plan` method in `ActionOrchestrator`

Read `src/praisonai/praisonai/cli/features/action_orchestrator.py:273-315`:

```python
async def approve_plan(self, plan: ActionPlan, auto: bool = False) -> bool:
    ...
    approval_mode = self.runtime.config.approval_mode         # line 288: always "auto"
    
    if approval_mode == "auto" or auto:                       # line 290: both True
        plan.status = ActionStatus.APPROVED
        for step in plan.steps:
            step.status = ActionStatus.APPROVED
        return True                                           # line 294: always returns True
```

**Analysis**: Line 290 has two conditions: `approval_mode == "auto"` (True because of the hardcoded override) OR `auto=True` (True because `auto_approve` was True from Step 4). Either condition alone would cause auto-approval. The method always returns `True`, enabling the plan to be applied.

### Step 6: The actual shell command execution

Read `src/praisonai/praisonai/cli/features/action_orchestrator.py:429-444` — `_apply_step()` for `SHELL_COMMAND`:

```python
elif step.action_type == ActionType.SHELL_COMMAND:
    import subprocess
    result = subprocess.run(
        step.target,                                          # line 432: user-controlled command
        shell=True,                                           # line 433: shell execution
        capture_output=True,
        text=True,
        cwd=str(workspace),
        timeout=30
    )
```

**Analysis**: `subprocess.run()` with `shell=True` executes `step.target` directly as a shell command. `step.target` comes from user input through the LLM agent. There is NO command sanitization in this path. No `_sanitize_command()` function exists in the codebase — the function referenced in the advisory at `agent_tools.py:60-86` does not exist in either `cli/features/agent_tools.py` or `code/agent_tools.py`.

### Step 7: Checking for alternative entry points that don't override

Searched all references to `approval_mode` across the codebase. Found:

- `src/praisonai/praisonai/cli/features/interactive_runtime.py:43` — `RuntimeConfig` defaults to `"manual"`
- `src/praisonai/praisonai/cli/features/interactive_tools.py:75` — `ToolConfig` defaults to `"auto"`
- `src/praisonai/praisonai/cli/features/interactive_tools.py:103` — reads `PRAISON_APPROVAL_MODE` from env
- `src/praisonai/praisonai/ui/chat.py:159` — HARDCODED to `"auto"`
- `src/praisonai/praisonai/ui/code.py:158` — HARDCODED to `"auto"`
- Examples in `examples/` directory — not production code

The only two places where `approval_mode` is hardcoded are `chat.py:159` and `code.py:158`, which are the two Chainlit UI entry points exposed to end users.

### Step 8: Checking code context — test/demo/dead code?

- `chat.py` is at `src/praisonai/praisonai/ui/chat.py` — production UI module, NOT in a test directory
- `code.py` is at `src/praisonai/praisonai/ui/code.py` — production UI module, NOT in a test directory
- Both are called when running `praisonai ui chat` or `praisonai ui code` — the main UI entry points
- Neither file is dead code; both have active Chainlit decorators (`@cl.on_chat_start`, `@cl.on_message`, `@cl.password_auth_callback`)

### Analysis Process

- **Q1: Is there a business constraint that could be violated?** → YES. The constraint is: `PRAISON_APPROVAL_MODE=manual` should require human approval before shell commands execute. This is violated by the hardcoded `"auto"` override at `chat.py:159` and `code.py:158`, which ignores the admin's configuration (see Steps 1-2).

- **Q2: Is the constraint enforced server-side?** → Partially. The framework at `interactive_tools.py:101-103` reads the env var correctly, and `action_orchestrator.py:288-315` properly enforces the approval mode logic. However, the UI layer at `chat.py:159` and `code.py:158` bypasses this enforcement by hardcoding `"auto"` (see Steps 3-5).

- **Q3: Can the user manipulate the relevant parameter in the request?** → Not directly, but this is a configuration-level bypass. An authenticated user can instruct the LLM to execute shell commands, and the admin's `PRAISON_APPROVAL_MODE=manual` setting is silently ignored (see Steps 4-6).

- **Q4: Is the manipulation filtered out (DTO, strong params, field whitelist)?** → No. No filtering or sanitization exists on the command execution path (Step 6). No `_sanitize_command()` function exists in the codebase.

- **Q5: Is the code in test/demo/dead/generated context?** → NO. Both `chat.py` and `code.py` are production Chainlit UI modules (Step 8).

→ **Reached leaf: TRUE_POSITIVE**

## 3. Verdict
**TRUE_POSITIVE**

**Key Evidence:**
- `src/praisonai/praisonai/ui/chat.py:159` — `config.approval_mode = "auto"` hardcoded after `ToolConfig.from_env()` reads `PRAISON_APPROVAL_MODE` from environment
- `src/praisonai/praisonai/ui/code.py:158` — identical hardcoded override in the code assistant UI
- `src/praisonai/praisonai/cli/features/interactive_tools.py:101-103` — confirms `from_env()` correctly supports `"auto"`, `"manual"`, `"scoped"` but the override discards it
- `src/praisonai/praisonai/cli/features/action_orchestrator.py:431-438` — `subprocess.run(step.target, shell=True)` executes arbitrary shell commands with no command sanitization

## 4. Remediation Suggestions

- **Remove the hardcoded override** in both `chat.py:159` and `code.py:158`. The lines `config.approval_mode = "auto"` should be deleted so that `PRAISON_APPROVAL_MODE` is respected. `ToolConfig.from_env()` already defaults to `"auto"` (line 75 of `interactive_tools.py`), so removing the override maintains backward-compatible behavior while allowing administrators to opt into `"manual"` or `"scoped"` modes.

- **Default to `"manual"` for UI modes**: Consider changing `ToolConfig.approval_mode` default from `"auto"` to `"manual"` at `interactive_tools.py:75` for the Chainlit UI, since these are user-facing interfaces where untrusted users might interact. The `RuntimeConfig` already defaults to `"manual"` at `interactive_runtime.py:43`.

- **Add command sanitization**: Implement an allowlist-based command filter (e.g., `shlex.split()` the command and verify the base command against an allowlist) in the `_apply_step()` method at `action_orchestrator.py:429-444` before executing `subprocess.run()`.

- **Use `shell=False` with argument lists** where possible, passing commands as `["cmd", "arg1", "arg2"]` instead of a raw shell string, to reduce the attack surface.

- **Add a command timeout** — the existing 30-second timeout at `action_orchestrator.py:437` is good but insufficient when combined with auto-approval and no sanitization.

