Integrations

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"

See also

Protected docs