fix(security): F01 (Astra P0) — KI-Tool-Ausführung ohne Freigabe verhindern
Check Cross-Plugin Imports / check (push) Has been cancelled

Vorher: _execute_tool (agent_loop.py) und der KI-Chat-Loop
(stream_chat_comm) führten JEDES im Registry registrierte Tool aus, wenn
das LLM dessen Namen lieferte — ohne Abgleich mit der angebotenen Liste,
ohne required_permission-Check. Reproduktion (Astra): Nur audit_allowed
angeboten, Modell nannte audit_restricted (system:admin) → Handler lief.

Fix (fail-closed, an ALLEN Ausfuehrungspfaden):
- _check_tool_access: (1) Allowlist — nur Tools die dem LLM angeboten
  wurden duerfen laufen; (2) required_permission gegen die AKTUELLEN
  User-Rechte (deny-first, Rechteentzug wirkt sofort, ohne Kontext =
  Ablehnung). Guard vor dry-run/approval/execute-Pfaden.
- stream_chat_comm: gleicher Allowlist-Guard vor execute_tool_call.
- run_react_loop/agent_runner/agent_stream/agent_routes reichen
  user_permissions durch (perm_ctx bzw. Session-User).
- check_permission: Session-Kontexte tragen denied_permissions statt
  denied — beide Keys werden gelesen, Deny-Liste wird nie mehr ignoriert.

Tests: test_agent_loop.py 18/18 (7 neue F01-Tests nach Astra-Abnahme:
nicht angeboten → Handler null; fehlende Permission → abgewiesen;
Fail-closed ohne Kontext; Deny-Liste session-shape; Rechteentzug
mitten im Lauf wirkt auf naechste Aktion; dry-run guardet auch).
ruff clean. Pre-existing-Beweis: permission_system_live-Failures
reproduzieren sich ohne diesen Patch identisch (Plugin-Aktivierung in
ephemeraler Test-DB, bekanntes Vorbestands-Finding).
This commit is contained in:
Agent Zero
2026-09-17 22:48:59 +02:00
parent 8a26737680
commit f2a7206c7d
7 changed files with 427 additions and 5 deletions
+4 -1
View File
@@ -20,7 +20,8 @@ import asyncio
import json
import logging
import uuid
from typing import TYPE_CHECKING, Any, AsyncGenerator
from collections.abc import AsyncGenerator
from typing import TYPE_CHECKING, Any
from app.ai.agent_loop import ReActStep, run_react_loop
@@ -71,6 +72,7 @@ async def stream_react_loop(
max_steps: int = 20,
timeout_seconds: int = 300,
trace_id: str | None = None,
user_permissions: dict[str, Any] | None = None,
) -> AsyncGenerator[str, None]:
"""Run the ReAct loop and yield SSE-formatted events.
@@ -116,6 +118,7 @@ async def stream_react_loop(
timeout_seconds=timeout_seconds,
trace_id=trace_id,
on_step=on_step,
user_permissions=user_permissions, # F01: enforce at execution time
)
await queue.put(
_sse(