raxit
Frameworks

CrewAI

Governing CrewAI agents in Python.

CrewAI's agent engine never calls your tool's _run directly — ToolUsage._use converts every crewai.tools.BaseTool via to_structured_tool() into a CrewStructuredTool and calls .invoke(...) on that wrapper. That one method is the point every real tool call flows through, so that's what raxit run patches: launch your existing CrewAI agent under it and every tool call is checked against policy — permit / defer / deny / modify — before the tool body runs, with zero raxit imports in your code. Python only (CrewAI has no JS/TS SDK).

Run it yourself

For your own CrewAI agent, install the SDK and launch through raxit run:

pip install raxit
raxit run --agent-id my-crew -- python my_crew_agent.py

Run the same file without raxit run and it runs ungoverned — governance is a deploy-time choice, not a code change.

The demo, step by step

A support-ops bot with five mock tools exercises every decision the engine can make. Every terminal line below is from the captured run:

CrewAI support-ops bot — permit / modify / defer / deny, zero-code

Five ordinary crewai.tools.BaseTool subclasses. Nothing in the agent imports raxit; each tool’s name is the policy resource it will be governed as.

crew_support_agent/tools.py
from crewai.tools import BaseTool

class SearchDocsTool(BaseTool):
    name: str = "search_docs"
    description: str = "Search internal support documentation (read-only)."

    def _run(self, query: str) -> str:
        ...

class DeleteCustomerRecordTool(BaseTool):
    name: str = "delete_customer_record"
    description: str = "Permanently delete a customer's record (policy forbids this)."

    def _run(self, customer_id: str) -> str:
        ...

What the demo drives

The scripted demo calls raxit.autopatch.install() directly — the exact function raxit run triggers under the hood — then dispatches each tool via to_structured_tool().invoke(...), the same path CrewAI's own orchestration uses. Deterministic and CI-able, minus the LLM.

How it works

The tools are ordinary crewai.tools.BaseTool subclasses with no governance code. The only "wiring" is each tool's name, which is the policy resource it's governed as in .raxit/security.yaml:

.raxit/security.yaml
apiVersion: raxit.security/v1
agent: crewai-bot
default: block
rules:
  - effect: allow
    resource: search_docs
  - effect: allow
    resource: update_ticket
  - resource: send_customer_email   # MODIFY: redact PII in the body before sending
    modify: { redact: [body] }
  - effect: review                  # unconditional review == defer.policy
    resource: issue_refund
  # delete_customer_record is intentionally UNLISTED -> default-deny.

Three things worth noticing:

  • default: block — anything not listed is denied. delete_customer_record has no rule, and that's the point: least privilege by omission.
  • modify: { redact: [body] } — the send_customer_email call runs, but on the engine's rewritten arguments. The agent passed body: "card 4242424242424242"; the tool body received 'card [REDACTED:pii_credit_card]'. The demo proves this from the tool's own call log, not from log output.
  • effect: reviewissue_refund always routes to a human. In an interactive raxit run session that blocks waiting for raxit approvals approve <token> (see Approvals); the scripted demo sets the defer timeout to zero so the raw defer surfaces immediately.

What got denied and why

Both refused calls raise raxit.autopatch.RaxitDenied from inside .invoke(), each carrying a named reason code — and in both cases the demo asserts the tool body never executed:

Tool callDecisionReason codeWhy
issue_refunddeferdefer.policyMatches an effect: review rule — needs human approval.
delete_customer_recorddenydeny.default_or_forbidUnlisted under default: block.

Catch RaxitDenied like any other tool exception, or let it propagate to CrewAI's own error handling. It carries the reason code and a call_id for raxit explain <call_id> — the full decision record, same as described in LangChain (Python). The reason-code vocabulary is covered in Decisions.

Honest scope: tool calls, not delegation

What this does not govern

raxit governs each agent's tool calls via the SDK autopatch. It does not govern CrewAI's inter-agent delegation between multiple Agents in a Crew — that surface is not reachable from the SDK tier. The demo deliberately runs a single-agent setup and claims nothing about multi-agent governance. Likewise, the autopatch proves raxit's known CrewAI dispatch point is live and governed — not that no bypass is possible.

  • Decisions — the permit / defer / deny vocabulary and reason codes.
  • Approvals — what happens after a defer.policy.
  • Audit — the hash-chained, Ed25519-signed Decision Provenance log the demo verifies in its finale.
  • LangChain (Python) — the reference patcher; same experience, different interception point.

On this page