Skip to content

[FEATURE] GuardrailProvider interface for pre-tool-call authorization #4877

@uchibeke

Description

@uchibeke

Feature Area

Core functionality

Is your feature request related to an existing bug?

Not a bug, but multiple open issues and PRs request tool-level authorization:

CrewAI's existing guardrail system (Task.guardrail / Task.guardrails) validates output after task completion. The BeforeToolCallHook protocol in crewai.hooks.types can block tool execution by returning False. What's missing is a standard provider contract that sits between the hook system and authorization logic, so users can plug in any policy engine without writing raw hooks.

Describe the solution you'd like

A GuardrailProvider protocol that any authorization provider can implement. It plugs into the existing BeforeToolCallHook system - no changes to the tool execution pipeline.

Interface (~40 lines)

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Protocol, runtime_checkable


@dataclass
class GuardrailRequest:
    """Context passed to the provider for each tool call."""
    tool_name: str
    tool_input: dict
    agent_role: str | None = None
    task_description: str | None = None
    crew_id: str | None = None
    timestamp: str = ""  # ISO 8601


@dataclass
class GuardrailDecision:
    """Provider's allow/deny verdict."""
    allow: bool
    reason: str | None = None
    metadata: dict = field(default_factory=dict)


@runtime_checkable
class GuardrailProvider(Protocol):
    """Contract for pluggable tool-call authorization."""

    name: str

    def evaluate(self, request: GuardrailRequest) -> GuardrailDecision:
        """Evaluate whether a tool call should proceed.

        Returns a GuardrailDecision. If allow is False, the tool call
        is blocked and `reason` is surfaced to the agent.
        """
        ...

    def health_check(self) -> bool:
        """Optional readiness probe. Default: True."""
        ...

How it plugs in

The provider registers itself as a BeforeToolCallHook. A thin adapter bridges the protocol:

from crewai.hooks.tool_hooks import register_before_tool_call_hook

def enable_guardrail(provider: GuardrailProvider, *, fail_closed: bool = True):
    """Wire a GuardrailProvider into CrewAI's hook system."""

    def _hook(context) -> bool | None:
        request = GuardrailRequest(
            tool_name=context.tool_name,
            tool_input=context.tool_input,
            agent_role=getattr(context.agent, "role", None),
            task_description=getattr(context.task, "description", None),
        )
        try:
            decision = provider.evaluate(request)
        except Exception:
            return False if fail_closed else None
        if not decision.allow:
            return False  # blocks tool execution
        return None  # allow

    register_before_tool_call_hook(_hook)

Configuration (optional, for YAML-based crews)

# crew.yaml or crewai config
guardrail_provider:
  enabled: true
  fail_closed: true
  provider: "my_package.MyGuardrailProvider"
  config:
    # provider-specific settings
    policy_file: "./policies/default.yaml"

What this enables

Simple (no external dependencies):

  • Block tools by name (e.g., deny ShellTool in production)
  • Restrict file paths per agent role
  • Rate-limit tool calls per crew run

Advanced (via provider packages):

  • Policy-as-code with declarative YAML rules
  • Per-agent capability scoping in multi-agent crews
  • Audit trails with signed decisions
  • Remote policy evaluation for enterprise deployments

Describe alternatives you've considered

1. Raw BeforeToolCallHook only (status quo)
Works today - any function returning False blocks a tool. But there's no contract for what context the hook receives, no standard for deny reasons, no fail_closed behavior, and no way to swap providers without rewriting hook logic. Each implementation is ad-hoc.

2. Extend Task guardrails to cover tool calls
Task guardrails (Task.guardrail) validate output after completion - a different concern. Tool-call authorization must happen before execution, per-call, across all tasks. Mixing the two conflates output validation with access control.

3. Middleware parameter on Agent (like #4682 proposes)
The loop detection middleware proposal (#4682) adds a middleware parameter to agents. A guardrail provider could be expressed as middleware, but tool authorization is cross-cutting - it should apply to all agents in a crew, not be configured per-agent. The hook system is the right level.

Additional context

Existing hook infrastructure is sufficient. The BeforeToolCallHook protocol in crewai.hooks.types already supports returning False to block execution. The ToolCallHookContext provides tool_name, tool_input, agent, task, and crew. The GuardrailProvider protocol is a standardization layer on top of this - not a new execution path.

Proven pattern. APort Agent Guardrails implements this provider pattern for multiple agent frameworks, demonstrating that a thin protocol over existing hooks is viable without core changes.

Scope boundary. This proposal covers: the GuardrailProvider protocol, the enable_guardrail() adapter, and documentation. It does NOT propose: RBAC, multi-tenant policies, bundled providers, changes to the agent loop, or modifications to existing task guardrails.

Willingness to Contribute

Yes, I'd be happy to submit a pull request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions