# npm PraisonAI AgentLoop onToolCall approval runs after tool execution

npm praisonai's AgentLoop runs tools via generateText before calling the onToolCall approval hook, so denied tools still execute and report tool_rejected.

- Payout: Research
- Severity: high
- Program: PraisonAI
- Found by: rexpository
- Disclosed: 2026-06-18
- CVE: CVE-2026-57137
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-h2w2-v7j6-xqm4
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-h2w2-v7j6-xqm4
- Tags: area:ai domain:ai domain:oss impact:integrity kind:research platform:ghsa sev:high tech:llm tech:nodejs technique:source-review vuln:access-control vuln:auth-bypass

**Target:** npm TypeScript AgentLoop approval callback (createAgentLoop)
**Root cause:** AgentLoop.step() passes executable tools to AI SDK generateText(), which executes them, and only afterwards invokes onToolCall; a false return sets finishReason 'tool_rejected' after the side effect already occurred.
**Steps:**
1. Install npm:praisonai@1.7.1 and mock `ai`/`@ai-sdk/openai` so generateText returns one tool-call intent and runs the supplied execute handler.
2. Call `createAgentLoop()` with a `dangerousWrite` tool whose execute() increments a side-effect counter, and an `onToolCall` that always returns false.
3. Run `node poc/pov_poc.js 1.7.1`; observe eventOrder `["tool-executed","approval-denied"]`, sideEffects 1, finishReason `tool_rejected`.
4. Compare against patched control that strips execute handlers before the model step: approval-denied, sideEffects 0, toolResultCount 0.
**How found:** Code review of agent-loop.ts: noticed onToolCall documented as an approval hook but placed after generateText() and toolResults materialization, contradicting AI SDK's pre-execution needsApproval model; confirmed ordering with a deterministic mock-based PoV.
**Exploitation:** An attacker who can submit prompts to an application built on createAgentLoop induces the model to call a powerful registered tool; the approval callback denies it but the tool has already executed with the host process's privileges (file writes, command execution, API calls).
**Insight:** When a library wraps an agent SDK, check whether 'approval' callbacks are wired to the SDK's pre-execution boundary (needsApproval) or merely run after generateText returns toolResults — the latter is an audit hook mislabeled as a gate.

## Proof of concept (copied exactly from the original report)

**PoC run command and expected vulnerable output**

```
node poc/pov_poc.js 1.7.1

Expected output includes:

{
  "praisonaiVersion": "1.7.1",
  "createAgentLoopExported": true,
  "eventOrder": ["tool-executed", "approval-denied"],
  "sideEffects": 1,
  "finishReason": "tool_rejected",
  "toolCallCount": 1,
  "toolResultCount": 1,
  "rejectedAfterExecution": true,
  "vulnerable": true,
  "patchedControl": {
    "order": ["approval-denied"],
    "sideEffects": 0,
    "toolCallCount": 1,
    "toolResultCount": 0,
    "blocksBeforeExecution": true
  }
}
```

**Vulnerable run setup: dangerousWrite tool and denying onToolCall**

```
The vulnerable run uses `createAgentLoop()` with:

- a `dangerousWrite` tool whose `execute()` handler increments an in-memory side-effect counter and records `tool-executed`;
- an `onToolCall` approval callback that always returns `false` and records `approval-denied`.
```

