A crewAI GuardrailProvider that turns every tool-call decision into a signed receipt.
Website | Docs | SDK | crewAI #4877
This package implements the GuardrailProvider protocol proposed in
crewAI #4877. Every
authorization decision (allow or deny) is recorded through
Asqav as a cryptographic receipt, and the receipt id is
returned with the verdict.
It plugs into crewAI's before_tool_call hook. When the provider denies a call,
the hook returns False and crewAI blocks the tool. When it allows, the call
proceeds and both outcomes carry a tamper-evident record of who authorized
what, and when.
asqav-crewai signs tool:start and tool:end events for audit. It observes
and records, and fails open by default so governance never breaks your crew.
This package is an authorization gate. It decides whether a tool call may
run, fails closed by default (a refused or unreachable Asqav blocks the call),
and produces a receipt for the decision itself, not just the event. Use the two
together: this provider gates the call, asqav-crewai records its start and
end.
Not yet on PyPI. Install from GitHub:
pip install "asqav-crewai-guardrail[crewai] @ git+https://github.com/jagmarques/asqav-crewai-guardrail.git"This pulls in the asqav SDK. crewAI is a peer dependency you install via the
crewai extra (shown above). Tool call hooks require crewAI 1.9.1 or newer.
import asqav
from crewai import Agent, Crew, Task
from crewai.tools import tool
from asqav_crewai_guardrail import AsqavGuardrailProvider, enable_guardrail
asqav.init(api_key="sk_...")
# Deny a small set of tools by name; allow everything else. Every decision,
# allow or deny, is recorded as an Asqav receipt.
provider = AsqavGuardrailProvider(
agent_name="my-crew",
denied_tools={"shell", "file_write"},
)
enable_guardrail(provider)
@tool("echo_tool")
def echo_tool(text: str) -> str:
"""Echo the given text back."""
return f"echoed {text}"
agent = Agent(
role="Echoer",
goal="Echo one message via the echo tool",
backstory="A minimal test agent.",
tools=[echo_tool],
)
task = Task(
description="Echo the message 'hello world' using echo_tool.",
expected_output="The echoed message.",
agent=agent,
)
result = Crew(agents=[agent], tasks=[task]).kickoff()Each tool call now flows through Asqav before it runs. The receipt is signed server-side with NIST FIPS 204 ML-DSA, so the audit trail is tamper-evident and holds up for EU AI Act, DORA, and SOC 2 evidence.
Asqav governs the agents you wire through it. An agent that never routes through the governed path produces no receipt and is not detected.
Asqav is the evidence layer. The decision can come from anywhere. Pass a
policy callback and Asqav signs whatever it returns:
def my_policy(request):
# slot in SINT, a YAML rules engine, a risk scorer, or anything else
if request.tool_name == "wire_transfer":
return (False, "blocked: regulated tool")
return (True, "on-allowlist")
provider = AsqavGuardrailProvider(agent_name="my-crew", policy=my_policy)
enable_guardrail(provider)The callback receives a GuardrailRequest and returns a (allow, reason)
tuple. The provider records the verdict as policy_decision="permit" or
"deny" on the receipt, so you get a chain of evidence no matter which engine
made the call.
If the Asqav API is unreachable, the provider cannot produce a receipt for the decision. By default it denies the call rather than act without a record:
AsqavGuardrailProvider(fail_closed=True) # default: block on Asqav error
AsqavGuardrailProvider(fail_closed=False) # proceed and drop the receiptThe hook adapter has its own fail_closed flag that governs what happens if
provider.evaluate itself raises unexpectedly:
enable_guardrail(provider, fail_closed=True) # block on provider errorThe crewAI #4877 discussion converged on a tri-state verdict rather than a
boolean. The protocol dataclass shipped here is the bi-state allow: bool from
the proposal, so this package is a drop-in once the protocol lands in crewAI
core. The escalate state is expressed as allow=False with a reason of
"awaiting approval". A subsequent human approval is recorded as a second
receipt countersigned onto the original (see Agent.countersign in the SDK), so
the human-in-the-loop lifecycle composes without new SDK surface.
AsqavGuardrailProvider extends the Asqav adapter base class. On each
evaluate():
- It runs the denylist, then the optional
policycallback, then a default allow-all, producing an(allow, reason)verdict. - It calls
Agent.sign(action_type="tool:authorize", ...)withpolicy_decision="permit"or"deny", the tool name, agent role, and the verdict reason. - It returns a
GuardrailDecisionwhosemetadata["receipt_id"]is the Asqav signature id for that decision.
enable_guardrail(provider) registers a before_tool_call hook that maps the
crewAI ToolCallHookContext into a GuardrailRequest, calls the provider, and
returns False to block when the decision denies.
asqav-crewai-guardrail is a thin wrapper around the asqav Python SDK and
inherits its mode behavior:
- Asqav cloud on
*.asqav.com: the SDK hashes your action context locally and sends only the hash plus a small metadata bag. Raw prompts and tool arguments never leave your infrastructure. - Self-hosted: the SDK sends the full context so the server can run policy checks, PII redaction, and richer audit views.
# Use an existing Asqav agent by ID
AsqavGuardrailProvider(agent_id="ag_abc123")
# Override the API key
AsqavGuardrailProvider(api_key="sk_other", agent_name="authz-crew")
# Observe only (no receipts written, decisions still returned)
AsqavGuardrailProvider(observe=True)Elastic License 2.0 (ELv2). See LICENSE.