Skip to content

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

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions