# Vulnerability Verification Report

## 1. Summary
- **Vulnerability Type**: Remote Code Execution (CWE-78)
- **Flagged Location**: `src/praisonai/praisonai/ui/chat.py:159` and `src/praisonai/praisonai/ui/code.py:158`
- **Vulnerability Description**: Chainlit UI code hardcodes `approval_mode = "auto"`, allegedly overriding administrator approval configuration and allowing authenticated users to auto-approve ACP shell command execution.

## 2. Analysis Logic

### Step 1: Examining the flagged approval-mode override in the Chat UI
I read the flagged Chat UI code. It does load `ToolConfig.from_env()`, then immediately overwrites the approval mode:

```python
# src/praisonai/praisonai/ui/chat.py:151-160
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()
            config.workspace = os.getcwd()
            config.approval_mode = "auto"  # Trust mode - auto-approve all tool executions
            tools = get_interactive_tools(config=config)
```

This confirms the configuration override exists in the Chat UI. The next question was whether that overridden value actually reaches an executable command sink.

### Step 2: Examining the same override in the Code UI
I read the flagged Code UI code and found the same pattern:

```python
# src/praisonai/praisonai/ui/code.py:150-159
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()
            config.workspace = os.environ.get("PRAISONAI_CODE_REPO_PATH", os.getcwd())
            config.approval_mode = "auto"  # Trust mode - auto-approve all tool executions
            tools = get_interactive_tools(config=config)
```

This confirms both flagged UI modules override the environment-derived approval mode before loading tools.

### Step 3: Verifying the administrator configuration source
I read the `ToolConfig` implementation. It supports `auto`, `manual`, and `scoped`, and it reads `PRAISON_APPROVAL_MODE` from the environment:

```python
# src/praisonai/praisonai/cli/features/interactive_tools.py:68-75
@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
```

```python
# src/praisonai/praisonai/cli/features/interactive_tools.py:100-105
# Check approval mode
approval = os.environ.get("PRAISON_APPROVAL_MODE", "")
if approval in ("auto", "manual", "scoped"):
    config.approval_mode = approval

return config
```

The override in Step 1 and Step 2 can therefore replace an administrator-supplied `manual` or `scoped` value with `auto`.

### Step 4: Tracing tool loading into the ACP runtime
I read the tool-loading implementation. `get_interactive_tools()` includes ACP tools when ACP is enabled:

```python
# src/praisonai/praisonai/cli/features/interactive_tools.py:313-316
# Load ACP tools (requires runtime)
if config.enable_acp and not (disable and "acp" in disable):
    acp_tools = _load_acp_tools(config)
    all_tools.update(acp_tools)
```

`_load_acp_tools()` copies the approval mode into a `RuntimeConfig`, creates an `InteractiveRuntime`, and creates agent tools from that runtime:

```python
# src/praisonai/praisonai/cli/features/interactive_tools.py:193-210
from .interactive_runtime import InteractiveRuntime, RuntimeConfig
from .agent_tools import create_agent_centric_tools

# Create runtime config
runtime_config = RuntimeConfig(
    workspace=config.workspace,
    lsp_enabled=config.lsp_enabled,
    acp_enabled=config.acp_enabled,
    approval_mode=config.approval_mode,
)

# Create runtime (lazy - doesn't start until needed)
runtime = InteractiveRuntime(runtime_config)

# Get all agent-centric tools
all_tools = create_agent_centric_tools(runtime)
```

The overridden value does flow into `RuntimeConfig.approval_mode`, but the same code explicitly only creates the runtime; it does not start it.

### Step 5: Tracing the user-controlled source into the agent tool path
I read the Chat UI message handler. The Chainlit user message is passed directly to the agent:

```python
# src/praisonai/praisonai/ui/chat.py:532-580
@cl.on_message
async def main(message: cl.Message):
    ...
    agent = _get_or_create_agent(model_name, tools_enabled) if tools_enabled else None
    
    if agent is not None:
        _profile_start("agent_response")
        await msg.send()
        
        try:
            # Use async chat for streaming
            result = await agent.achat(message.content)
```

The Code UI has the same source-to-agent handoff:

```python
# src/praisonai/praisonai/ui/code.py:578-632
@cl.on_message
async def main(message: cl.Message):
    ...
    agent = _get_or_create_agent(model_name, tools_enabled, claude_code_enabled) if tools_enabled else None
    
    if agent is not None:
        _profile_start("agent_response")
        await msg.send()
        
        try:
            # Use async chat for streaming
            result = await agent.achat(message.content)
```

I also read the agent execution code. `achat()` supplies the agent's tools and `execute_tool_async` to the LLM wrapper:

```python
# src/praisonai-agents/praisonaiagents/agent/agent.py:2642-2698
# Default to self.tools if tools argument is None
if tools is None:
    tools = self.tools
...
response_text = await self.llm_instance.get_response_async(
    prompt=prompt,
    system_prompt=self._build_system_prompt(tools),
    chat_history=self.chat_history,
    temperature=temperature,
    tools=tools,
    ...
    execute_tool_fn=self.execute_tool_async,
    reasoning_steps=reasoning_steps
)
```

And `execute_tool_async()` resolves a callable by name from `self.tools` and invokes it with LLM-supplied arguments:

```python
# src/praisonai-agents/praisonaiagents/agent/agent.py:3505-3523
# Try to find the function in the agent's tools list first
func = None
for tool in self.tools:
    if (callable(tool) and getattr(tool, '__name__', '') == function_name):
        func = tool
        break
...
if inspect.iscoroutinefunction(func):
    logging.debug(f"Executing async function: {function_name}")
    result = await func(**arguments)
else:
    logging.debug(f"Executing sync function in executor: {function_name}")
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(None, lambda: func(**arguments))
```

This establishes that a user prompt can lead to a callable tool invocation if the LLM chooses the tool. I then traced the specific `acp_execute_command` tool.

### Step 6: Tracing `acp_execute_command()` and the approval decision
I read `acp_execute_command()`. It constructs a prompt from the command, creates a plan, and checks `runtime.config.approval_mode == "auto"` before approval:

```python
# src/praisonai/praisonai/cli/features/agent_tools.py:225-250
def acp_execute_command(command: str, cwd: str = None) -> str:
    """
    Execute a shell command through ACP with tracking.
    ...
    """
    async def _execute():
        prompt = f"run command: {command}"
        
        result = await orchestrator.create_plan(prompt)
        if not result.success:
            return json.dumps({
                "success": False,
                "error": result.error
            })
        
        plan = result.plan
        
        # Commands require approval
        auto_approve = runtime.config.approval_mode == "auto"
        approved = await orchestrator.approve_plan(plan, auto=auto_approve)
```

If `create_plan()` succeeds and the plan is approved, the tool would apply the plan:

```python
# src/praisonai/praisonai/cli/features/agent_tools.py:252-260
if not approved:
    return json.dumps({
        "success": False,
        "error": "Command requires approval",
        "requires_approval": True
    })

result = await orchestrator.apply_plan(plan)
```

At this point, the overridden `approval_mode = "auto"` would matter only if `create_plan()` can succeed and `apply_plan()` can run.

### Step 7: Identifying the command execution sink
I read the sink in `ActionOrchestrator._apply_step()`. For `SHELL_COMMAND`, it executes `step.target` through a shell:

```python
# src/praisonai/praisonai/cli/features/action_orchestrator.py:429-438
elif step.action_type == ActionType.SHELL_COMMAND:
    import subprocess
    result = subprocess.run(
        step.target,
        shell=True,
        capture_output=True,
        text=True,
        cwd=str(workspace),
        timeout=30
    )
```

This is an unsafe process-execution sink in isolation: it uses a shell string (`shell=True`) rather than array/list form. I then checked whether the UI-created runtime permits execution to reach this sink.

### Step 8: Finding the definitive break in the UI source-to-sink path
The UI-created `InteractiveRuntime` starts with ACP not ready. The runtime constructor initializes `_acp_state` with the default `SubsystemState`, and `SubsystemState.status` defaults to `NOT_STARTED`:

```python
# src/praisonai/praisonai/cli/features/interactive_runtime.py:28-32
@dataclass
class SubsystemState:
    """State of a subsystem."""
    status: SubsystemStatus = SubsystemStatus.NOT_STARTED
    error: Optional[str] = None
```

```python
# src/praisonai/praisonai/cli/features/interactive_runtime.py:124-132
def __init__(self, config: RuntimeConfig = None):
    """Initialize the runtime."""
    self.config = config or RuntimeConfig()
    self._lsp_state = SubsystemState()
    self._acp_state = SubsystemState()
    self._lsp_client = None
    self._acp_session = None
    self._trace = RuntimeTrace(config=self.config) if self.config.trace_enabled else None
    self._started = False
```

`acp_ready` is true only when `_acp_state.status == READY`, and `read_only` is true whenever ACP is not ready:

```python
# src/praisonai/praisonai/cli/features/interactive_runtime.py:140-148
@property
def acp_ready(self) -> bool:
    """Check if ACP is ready."""
    return self._acp_state.status == SubsystemStatus.READY

@property
def read_only(self) -> bool:
    """Check if runtime is in read-only mode."""
    return self._read_only or not self.acp_ready
```

The only code that changes ACP to ready is `start()`, via `_start_acp()`:

```python
# src/praisonai/praisonai/cli/features/interactive_runtime.py:171-191
async def start(self) -> Dict[str, Any]:
    ...
    # Start subsystems in parallel
    tasks = []
    if self.config.lsp_enabled:
        tasks.append(self._start_lsp())
    if self.config.acp_enabled:
        tasks.append(self._start_acp())
```

```python
# src/praisonai/praisonai/cli/features/interactive_runtime.py:248-265
async def _start_acp(self):
    """Start ACP subsystem (in-process session)."""
    self._acp_state.status = SubsystemStatus.STARTING
    ...
    self._acp_session = ACPSession.create(
        workspace=Path(self.config.workspace)
    )
    self._acp_session.mode = self.config.approval_mode
    
    self._acp_state.status = SubsystemStatus.READY
```

But the UI tool-loading path only constructs `InteractiveRuntime`; it never calls `runtime.start()` before returning the ACP tools:

```python
# src/praisonai/praisonai/cli/features/interactive_tools.py:198-210
runtime_config = RuntimeConfig(
    workspace=config.workspace,
    lsp_enabled=config.lsp_enabled,
    acp_enabled=config.acp_enabled,
    approval_mode=config.approval_mode,
)

# Create runtime (lazy - doesn't start until needed)
runtime = InteractiveRuntime(runtime_config)

# Get all agent-centric tools
all_tools = create_agent_centric_tools(runtime)
```

Because that runtime is not started, `ActionOrchestrator.create_plan()` returns before any approval or apply step:

```python
# src/praisonai/praisonai/cli/features/action_orchestrator.py:138-153
async def create_plan(self, prompt: str) -> ActionResult:
    """
    Create an action plan for a modification request.
    ...
    """
    if self.runtime.read_only:
        return ActionResult(
            success=False,
            error="Runtime is in read-only mode. ACP is required for modifications.",
            read_only_blocked=True
        )
```

This is the decisive break in the claimed RCE path: in the flagged Chainlit UI path, `acp_execute_command()` reaches `orchestrator.create_plan()`, but `create_plan()` returns an error while the runtime is read-only. The code therefore does not reach `approve_plan()`, `apply_plan()`, or the `subprocess.run(..., shell=True)` sink.

### Step 9: Checking approval and dependency context
The separate agent-level approval registry does not block `acp_execute_command`, because its default dangerous tools include `execute_command` but not `acp_execute_command`:

```python
# src/praisonai-agents/praisonaiagents/approval.py:227-240
DEFAULT_DANGEROUS_TOOLS = {
    # Critical risk tools
    "execute_command": "critical",
    "kill_process": "critical", 
    "execute_code": "critical",
    
    # High risk tools
    "write_file": "high",
    "delete_file": "high",
    "move_file": "high",
    "copy_file": "high",
    "execute_query": "high",
```

```python
# src/praisonai-agents/praisonaiagents/approval.py:247-265
def configure_default_approvals():
    """Configure default dangerous tools to require approval."""
    for tool_name, risk_level in DEFAULT_DANGEROUS_TOOLS.items():
        APPROVAL_REQUIRED_TOOLS.add(tool_name)
        TOOL_RISK_LEVELS[tool_name] = risk_level
...
def is_approval_required(tool_name: str) -> bool:
    """Check if a tool requires approval."""
    return tool_name in APPROVAL_REQUIRED_TOOLS
```

The UI dependencies confirm this is production application code, not a test-only path: the package defines Chainlit UI extras for chat and code modes:

```toml
# src/praisonai/pyproject.toml:49-63
chat = [
    "chainlit>=2.8.5,<2.9.0",
    "litellm>=1.72.6",
    ...
]
code = [
    "chainlit>=2.8.5,<2.9.0",
    "litellm>=1.72.6",
```

And the CLI launches the same `chat.py` and `code.py` UI modules:

```python
# src/praisonai/praisonai/cli/main.py:4492-4503
def create_chainlit_chat_interface(self):
    """
    Create a Chainlit interface for the chat application.
    """
    if CHAINLIT_AVAILABLE:
        import praisonai
        os.environ["CHAINLIT_PORT"] = "8084"
        ...
        chat_ui_path = os.path.join(os.path.dirname(praisonai.__file__), 'ui', 'chat.py')
        _get_chainlit_run()([chat_ui_path])
```

```python
# src/praisonai/praisonai/cli/main.py:4507-4527
def create_code_interface(self):
    """
    Create a Chainlit interface for the code application.
    """
    if CHAINLIT_AVAILABLE:
        import praisonai
        os.environ["CHAINLIT_PORT"] = "8086"
        ...
        code_ui_path = os.path.join(os.path.dirname(praisonai.__file__), 'ui', 'code.py')
        _get_chainlit_run()([code_ui_path])
```

So the code is not excluded as test/demo code; the false positive rests on the source-to-sink break caused by the unstared ACP runtime and read-only guard.

### Analysis Process
- Q1: Does user-controllable input reach a command/code execution sink? → **NO for the flagged UI path.** User input reaches `agent.achat(message.content)` in `chat.py:532-580` and `code.py:578-632`, and the agent can invoke callable tools via `execute_tool_async()` in `agent.py:3505-3523`; however, the `acp_execute_command()` path stops at `ActionOrchestrator.create_plan()` because `runtime.read_only` is true (`action_orchestrator.py:138-153`) before the sink in `action_orchestrator.py:429-438`.
- Q2: Is it code evaluation (`eval`/`exec`) with user data? → **NO.** The relevant sink is process execution with `subprocess.run(step.target, shell=True)` at `action_orchestrator.py:429-438`, not Python code evaluation.
- Q3: Is the command executed in array form without shell? → **NO.** The sink uses shell string execution: `step.target` with `shell=True` at `action_orchestrator.py:431-438`.
- Q4: Is the user input effectively sanitized? → **NO sanitizer is reached or needed for the UI path.** The plan creation returns before approval or application when `runtime.read_only` is true (`action_orchestrator.py:138-153`).
- Q5: Is the code in test/demo/dead/generated context? → **NO.** `pyproject.toml:49-63` defines Chainlit chat/code extras, and `cli/main.py:4492-4527` launches the production UI modules.
- → Reached leaf: **FALSE_POSITIVE**, because the RCE decision tree stops at Q1: the flagged UI source does not reach the command execution sink.

## 3. Verdict
**FALSE_POSITIVE**

**Key Evidence:**
- `src/praisonai/praisonai/ui/chat.py:157-160` and `src/praisonai/praisonai/ui/code.py:156-159` do hardcode `config.approval_mode = "auto"` after `ToolConfig.from_env()`.
- `src/praisonai/praisonai/cli/features/interactive_tools.py:198-210` creates an `InteractiveRuntime` for those tools but does not call `runtime.start()`.
- `src/praisonai/praisonai/cli/features/interactive_runtime.py:140-148` makes an unstared runtime read-only because ACP is not ready, and `src/praisonai/praisonai/cli/features/action_orchestrator.py:138-153` returns an error before command plans can be approved or applied.
- The dangerous sink exists at `src/praisonai/praisonai/cli/features/action_orchestrator.py:429-438`, but the flagged Chainlit UI path does not reach it.

## 4. Remediation Suggestions
- No code change is required to remediate this specific RCE alert because the flagged UI path does not reach command execution.
- Optional hardening: remove the hardcoded `approval_mode = "auto"` in `chat.py` and `code.py` to avoid misleading administrators and to prevent this from becoming exploitable if the ACP runtime is later started in the UI path.
- Optional hardening: if ACP command execution is intentionally enabled later, keep `PRAISON_APPROVAL_MODE=manual` or `scoped` respected, and avoid `subprocess.run(..., shell=True)` for user-influenced commands.
