Skip to Content

CrewAI

CrewAI decides which agent does what. AgentValet decides what any of them is allowed to reach.

A normal CrewAI tool holds the downstream credential: to post to Slack, something in your process needs a Slack token. Put AgentValet in front and the crew holds no platform credential at all. It holds one identity key, and every outbound call is checked against your grants, approved where you require it, and written to the audit log.

There are two ways to wire it up. Start with the first.

AgentValet publishes an MCP server, and CrewAI speaks MCP. Point one at the other.

from crewai import Agent from crewai.mcp import MCPServerStdio agent = Agent( role="Operations Engineer", goal="Keep the team informed and the tracker up to date", backstory="You handle routine ops chores end to end.", mcps=[ MCPServerStdio( command="npx", args=["-y", "@agentvalet/mcp-server"], env={ "AGENT_ID": os.environ["AGENT_ID"], "OWNER_ID": os.environ["OWNER_ID"], "AGENT_PRIVATE_KEY_PATH": os.path.expanduser("~/.agentvalet/agent.key"), }, ), ], )

That is the whole integration. The crew now has tools for every platform you have granted this agent, and no tools for anything you have not.

If you have not registered an agent yet, do that first. It takes one command and generates the keypair on your machine:

pip install agentvalet agentvalet register --code <invite-or-enrollment-code>

The private half never leaves the machine. Only the public key is sent.

Why stdio rather than a URL

AgentValet also exposes a remote MCP endpoint, and MCPServerHTTP will connect to it. But agent assertions are deliberately short-lived: 60 seconds. MCPServerHTTP(headers=...) takes its headers once, at construction, so a static Authorization header goes stale a minute into the run and every later tool call fails.

The stdio server signs a fresh assertion for each request, so it keeps working for as long as the crew runs. Use the remote endpoint only where you have somewhere to refresh the token from, and treat the 60-second lifetime as a design constraint rather than a detail.

Option 2: as ordinary CrewAI tools

If you would rather not run an MCP server — no Node, no subprocess — install the CrewAI package and let it build the tools for you.

pip install crewai-agentvalet
from crewai import Agent from crewai_agentvalet import governed_tools agent = Agent( role="Operations Engineer", goal="Keep the team informed and the tracker up to date", backstory="You handle routine ops chores end to end.", tools=governed_tools(), )

governed_tools() asks the broker what this agent is granted and returns one typed CrewAI tool per platform, in a stable order so the prompt does not churn between runs. It is the same enforcement boundary as Option 1, reached without MCP.

Narrow it further with platforms=[...] when a role should see less than it is granted. That argument can only ever subtract: naming a platform you were never granted skips it rather than inventing it. Pass on_skip= to find out why a tool you expected is missing, without going to the audit log:

tools = governed_tools( platforms=["slack", "linear"], on_skip=lambda platform, reason: print(f"skipped {platform}: {reason}"), )

Importing crewai_agentvalet does not pull in CrewAI itself — the CrewAI-facing names resolve on first use, so the permissions layer stays usable and testable on its own.

Writing a tool by hand

If you need a shape the generated tools do not cover, call the broker directly from a BaseTool. The Python client signs the assertion, brokers the call, and waits out an approval if your policy requires one.

from crewai.tools import BaseTool from pydantic import BaseModel, Field from agentvalet import AgentValet av = AgentValet.from_env() class PostMessage(BaseModel): channel: str = Field(description="Channel to post in, e.g. #general") text: str = Field(description="Message body") class SlackPostTool(BaseTool): name: str = "post_to_slack" description: str = "Post a message to a Slack channel." args_schema: type[BaseModel] = PostMessage def _run(self, channel: str, text: str) -> str: result = av.call( platform="slack", endpoint="/api/chat.postMessage", method="POST", scope="chat:write", data={"channel": channel, "text": text}, ) return str(result)

Either way the credential is never in this file, never in your environment, and never in the model’s context. It is decrypted in memory inside AgentValet at call time.

What the crew can actually see

The tool list is not a convenience filter you configure in your own code. It is the enforcement boundary, computed on the server from your grants and policy.

An agent cannot call a platform you have not granted it, and cannot use a scope your policy denies, because that scope is never in the list it receives. Tightening a grant narrows the crew’s tool surface. There is no client-side allow-list to keep in sync, and no way for a confused agent to talk itself into a call that was never on the table.

Approvals in the middle of a run

Where your policy says a scope needs a human, the call does not fail. It parks.

The tool blocks while AgentValet notifies you, and you approve or decline from the dashboard, an email, or your phone. If you approve, the call executes and the tool returns its result, and the crew carries on with no idea anything unusual happened. If you decline, the tool returns a refusal the agent can reason about.

The wait is bounded at 50 seconds. Past that the tool returns rather than hanging, and the action stays queued: approving it later still runs it. For a crew step that must not be abandoned, catch the timeout and resume from the approval id rather than retrying the call, which would queue a second copy.

This is per action. It is finer-grained than CrewAI’s task-level human_input, and the two compose: use human_input when a human should shape the work, and an AgentValet approval when a human should authorise a specific outbound effect.

One caveat worth knowing

If you use the remote MCP endpoint, the crew learns the tool list once, when it connects. That transport cannot push a change notification, so a grant you revoke mid-run is not advertised to a crew already running.

It is still enforced. The call is refused at the broker whatever the crew believes it can do. But the agent may waste a step discovering that. For long-lived crews, prefer the stdio server, which revalidates cheaply as it goes.

What gets recorded

Every call through AgentValet writes an audit row: which agent, which platform, which scope, which endpoint, and what the decision was. Not just what succeeded, but what was denied, what waited for a human, and what a human said.

For a crew, that is usually the missing half of the picture. Your CrewAI logs tell you what the agents decided to do. The audit log tells you what they were permitted to do, which is the question you get asked after something goes wrong.

Multiple agents in one crew

Give each role its own AgentValet agent when their authority should differ. A researcher that only reads and a publisher that can post are two identities with two grant sets, and the audit trail then tells you which role did what.

Register them separately from the dashboard. Note that agent-to-agent delegation, where one agent’s authority is derived from another’s, is not something AgentValet enforces today: each agent’s permissions are the ones you granted it directly, so grant the narrow ones narrowly.

Next steps

Last updated on