CrewAI
Route a CrewAI crew through Sentisec — the base-URL override, the flag that pins it, and where a blocked tool call actually shows up for a crew.
CrewAI drives its agents through its own LLM wrapper rather than a raw
provider client, so the base-URL override needs one extra argument. With it,
every agent in the crew — and every tool call any of them tries — goes
through Sentisec.
The swap
from crewai import LLM
llm = LLM(
model="gpt-5.6-terra",
base_url="https://api.sentisec.ch/ws/<your-workspace-id>/v1",
api_key="sk_sentisec_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
custom_openai=True,
)Use the OpenAI-shaped base URL whichever model you name. CrewAI speaks
the OpenAI Chat Completions shape for every model, so the /v1 suffix
belongs on the URL even if you name an Anthropic model. Pointing it at the
Anthropic-shaped URL produces a 404 at the first call. The workspace id in
the URL is what carries your policy;
why it matters.
Set custom_openai=True. With an OpenAI model name, base_url is
honoured with or without it — but the flag pins CrewAI to your endpoint
instead of letting it infer a provider from the model name. That removes the
failure mode where renaming a model silently reroutes your traffic around
Sentisec. It also fails loudly: the constructor raises if you set the flag
with no base URL, so a misconfiguration surfaces at startup rather than as
unmonitored traffic.
A crew
Pass the llm to each agent. Nothing else about your crew changes.
from crewai import Agent, Crew, Task
from crewai.tools import tool
@tool("read_report")
def read_report(path: str) -> str:
"""Read a report from the finance share."""
return open(path).read()
@tool("run_shell")
def run_shell(command: str) -> str:
"""Run a shell command."""
return subprocess.run(command, shell=True, capture_output=True, text=True).stdout
agent = Agent(
role="Ledger reconciler",
goal="Reconcile the quarterly ledger",
backstory="You reconcile finance ledgers.",
tools=[read_report, run_shell],
llm=llm,
max_iter=4,
)
task = Task(
description="Reconcile the quarterly ledger using the report.",
expected_output="A one-line reconciliation summary.",
agent=agent,
)
result = Crew(agents=[agent], tasks=[task]).kickoff()What the crew does when a tool call is blocked
Take the classic indirect-injection chain: the agent reads a document carrying a planted instruction, then tries to run a shell command lifted out of that document. Sentisec stops the shell call.
By default the crew ends with the explanation, and the tool never
runs. Sentisec withholds the call and puts the explanation in the
model's message. With no tool call left, CrewAI takes that message as the
final answer, so kickoff() returns it:
raw output: [Sentisec blocked tool_use: Sentisec policy blocked this tool call. ...]If your application shows the crew's output to a user, expect that sentence there. The run is also recorded on the dashboard, with the chain that led to the block.
With the rename opt-in, the agent re-plans. The call comes back as
sentisec_tool_call_blocked, CrewAI answers it as an unknown tool, and
Sentisec replaces that answer with its ruling before the model reads it.
The crew then finishes another way and kickoff() returns its expected
output. CrewAI hides intermediate tool calls, so nothing on the returned
object tells you a block happened: the dashboard is your record of it.
This works on every API shape except streamed Responses, where CrewAI
fails on every streamed tool call with or without Sentisec. See the
supported versions.
Multi-agent crews
Every agent that receives the same llm is covered. If different agents use
different models, each needs its own LLM(...) with the same base-URL
treatment — an agent constructed with a default LLM bypasses Sentisec
entirely. A quick audit: every Agent(...) in your crew should have an
explicit llm=.
Telemetry
CrewAI ships its own tracing, independent of Sentisec's. If you run in an
environment where outbound telemetry is restricted, disable it before
kickoff():
import os
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
os.environ["OTEL_SDK_DISABLED"] = "true"