Onyx Gate for CrewAI
A verifiable authorization gate for CrewAI tool calls. Wrap a crew's tools and every execution is decided by policy before it runs — outside the model, in a separate process the agent cannot talk its way past. Denied calls never execute; the agent receives the reason and re-plans. Every decision lands in a hash-chained audit trail you can re-check offline, with an optional machine-checkable proof of each decision attached.
No changes to CrewAI, no changes to your tools: the guard is a wrapper.
from crewai import Agent
from onyx_gate_crewai import OnyxGate, ToolGuard, guard_tools
guard = ToolGuard(OnyxGate("http://127.0.0.1:8080"), agent="ap-clerk")
clerk = Agent(
role="Accounts-payable clerk",
goal="Settle the invoices that are due",
backstory="…",
tools=guard_tools([read_ledger, pay_invoice], guard=guard), # ← the whole integration
)
What it looks like
The finance crew example gives an agent a ledger and a payment tool, under this policy (Cedar, an allowlist — anything unmatched is denied by default):
entity Agent::"ap-clerk" {} ;
// Reading the ledger is harmless — always permitted.
permit(principal, action == Action::"read_ledger", resource);
// Payments are permitted ONLY inside the mandate: an approved vendor AND
// at-or-under the autonomous cap. Anything outside needs a human.
permit(principal, action == Action::"pay_invoice", resource)
when {
resource.amount <= 5000 &&
(resource.vendor == "globex" || resource.vendor == "initech")
};
The agent is told to pay every invoice that is due. Two are inside the mandate; the third is not. Real output (deterministic dry run, real gateway):
[gate 0.5 ms] ALLOW pay_invoice {'vendor': 'initech', 'amount': 1250, 'invoice_id': 'INV-1041'}
Payment sent: $1250 to initech for INV-1041.
[gate 0.5 ms] ALLOW pay_invoice {'vendor': 'globex', 'amount': 4800, 'invoice_id': 'INV-1042'}
Payment sent: $4800 to globex for INV-1042.
[gate 0.5 ms] DENY pay_invoice {'vendor': 'vandelay', 'amount': 12000, 'invoice_id': 'INV-1043'}
--- the agent sees ----------------------------------------
[Onyx Gate] DENIED — this tool call was blocked and did NOT execute.
call: pay_invoice(vendor='vandelay', amount=12000, invoice_id='INV-1043')
reason: Denied — no permit policy matches (Cedar's default deny).
The gate's decision is final for this exact call. Do not retry it with the
same arguments; adjust the plan — use a permitted route, or report that this
step needs human approval.
-----------------------------------------------------------
Executed payments: initech ($1250), globex ($4800)
The $12,000 payment did not execute — not "the model chose not to": the tool body was never entered. The deny message the agent reads names the exact call and carries the policy's reason, which is what lets the agent recover sensibly (report it for human approval) instead of thrashing.
Why a gate outside the model
Prompted guardrails are advice; a strong model under a persuasive context can rationalize its way around them. In our measured pilot on a seeded-mistake task family, agents corrected course after a bare advisory verdict in 8% of flagged runs, after a verdict with the policy's reason in 44% — and a hard block corrected 100%, with the agents completing their tasks through permitted routes (one task family, n=20 per arm, driver-model dependent; details at onyxfoundry.ai). That gradient is this integration's design: enforce by default, always carry the reason, and keep the decision outside the agent's process entirely.
How it works
CrewAI agent ──picks tool──▶ OnyxGuardedTool ──POST /gate/tool-call──▶ Onyx gateway
│ (policy engine,
allow ────────┤◀── {"decision", "explanation"} ──── audit trail)
runs the tool │
deny: returns the deny message as the observation —
the tool body never runs
Each call maps onto a Cedar authorization request:
| tool call part | Cedar request part |
|---|---|
the guard's agent name |
principal = Agent::"ap-clerk" |
| the tool's name | action = Action::"pay_invoice" |
| the call itself | resource = Tool::"pay_invoice" |
| each argument | a resource attribute — resource.vendor, resource.amount, … |
guard-level context={...} |
context.env, … |
Argument mapping is designed so every argument reaches the policy: strings,
booleans and 64-bit integers pass verbatim; floats and oversized integers are
sent as strings; nested structures are serialized to compact JSON so substring
(like) policies still see their content. Nothing is silently dropped or
truncated on the wire — a policy keyed on an argument the gate can't see would
fail open.
Modes and failure behavior
| situation | mode="enforce" (default) |
mode="observe" |
|---|---|---|
| gate allows | tool runs | tool runs |
| gate denies | blocked; agent gets the reason | tool runs; a note naming the flagged call is appended |
| gateway unreachable / errors | blocked (fail-closed; on_error="allow" opts out, annotated) |
tool runs, annotated |
Observe mode is for the pilot phase: nothing is blocked, but the gateway still records every would-deny in the trail, so you can measure what enforcement would do on real traffic before turning it on.
One guard = one agent identity. Wrap each crew agent's tools with a guard carrying that agent's name, and the trail attributes every call correctly:
researcher_tools = guard_tools([search, fetch], gate=gate, agent="researcher")
clerk_tools = guard_tools([read_ledger, pay_invoice], gate=gate, agent="ap-clerk")
The audit trail
Start the gateway with --log and every decision — allow and deny, with the
full call arguments — is appended to a hash-chained JSONL trail (each record
links the SHA-256 of the previous one). eg_verify re-checks the whole file
offline:
$ eg_verify --audit-log trail.jsonl
record 5 gate_tool_call ✓ confirmed (1 step(s))
record 6 gate_tool_call ✓ confirmed (1 step(s))
record 7 gate_tool_call ✓ confirmed (1 step(s))
record 8 gate_tool_call ✓ confirmed (1 step(s))
re-checked 8 record(s): 4 confirmed, 0 failed, 4 without a certificate
chain: 8 linked, 0 unchained, 0 broken
% Onyx audit-log: Confirmed
Edit one byte of one record — say, quietly changing the denied amount — and
the re-check fails loudly:
chain: recorded link 2ea6e5fe… does not match the expected 3c03da7e… —
the record before this one was edited, deleted, or reordered
chain: 7 linked, 0 unchained, 1 broken
% Onyx audit-log: Failed
Decision certificates
Pass certify=True (per guard or per call) and each decision comes back with a
certificate: a self-contained record of the policy reasoning behind that exact
verdict, which eg_verify re-checks with the engine's small proof kernel
alone — not by trusting the gateway that issued it. In the run above, the
certified decisions cost ~2–3 ms instead of ~0.5–1 ms (local loopback, single
client, Apple-silicon laptop). That is what "verifiable" means here: the audit
answer is not "the logs say so" but "re-derive it yourself, offline."
Try it
pip install "onyx-gate-crewai[crewai] @ git+https://github.com/theonyxfoundry/onyx-gate-crewai"
Without the engine — the test suite runs against a scripted stub gateway, so you can see the integration shape immediately:
git clone https://github.com/theonyxfoundry/onyx-gate-crewai && cd onyx-gate-crewai
pip install -e '.[dev,crewai]' && pytest
With the engine (design-partner preview — see below):
eg_gateway --policies examples/finance_crew/policy.cedar --log trail.jsonl &
python examples/finance_crew/dry_run.py # deterministic, no LLM key needed
python examples/finance_crew/crew.py # the live crew (any CrewAI-supported LLM)
eg_verify --audit-log trail.jsonl # re-check the trail offline
Policy tips
- Prefer the allowlist style shown above (
permit … when { … }, no catch-all permit). A permit whose condition errors — e.g. a required argument is missing — simply doesn't match, so the call is denied: fail-closed. A blocklist (permit(principal, action, resource);plusforbids) is easy to start with but aforbid unless { … }whose condition errors skips the forbid — fail-open. Reach forforbidwhen you want a rule no permit can override. - Policies can also key on tool taxonomies (
action in ActionGroup::"…"via the gateway's--action-groups), resource hierarchies (resource_parents), and per-guardcontextfields.
Scope, honestly
- This is an authorization boundary, not a sandbox. The guard governs the agent's tool calls in-process. It does not contain a malicious tool implementation, and code that bypasses the wrapped tools bypasses the guard. Pair it with OS-level sandboxing where that threat matters. (The same engine also ships an enforcing PreToolUse hook for Claude Code, which blocks at the harness rather than in-process.)
- The gateway decides; it never executes. Onyx holds no keys and moves no funds — it answers allow/deny and proves its answers.
- Certificates are kernel-re-checkable records of the policy reasoning — re-derivable by an independent checker; they are not digital signatures, and the trail's hash chain detects edits, not deletion of the whole file (anchor the chain head externally for that).
- Latency figures above are measured on the example, not a benchmark suite.
Getting the gateway
The wire protocol is fully documented here and stubbed in
tests/stub_gateway.py; this package is Apache-2.0
and yours to use. The gateway binary (eg_gateway) and offline checker
(eg_verify) are part of the Onyx engine — a verification-first policy
engine whose decision calculus is machine-checked in two independent proof
assistants — currently in design-partner preview.
Running agents that touch money, records, or customers, and want the pilot shape you saw above (observe mode → measured would-denies → enforcement, with a re-checkable trail)? contact@onyxfoundry.ai · onyxfoundry.ai
Supported gateway versions
This client was written against Onyx gateways ≥ 0.2.0 and < 1.0.0 — the Onyx
product version, which the gateway reports as server.version on GET /ready
(since 0.2.0). The HTTP paths it speaks are stable, so this is a soft check:
when a guard is built it reads the gateway's version once and emits a
RuntimeWarning outside that range — never a refusal — so the two can be
upgraded together. ToolGuard(..., check_version=False) skips the startup
request; a gateway older than 0.2.0 reports no version and is treated as
unknown (silent). OnyxGate.server_info() returns the block for your own logs.
License
Apache-2.0. CrewAI is a trademark of its respective owner; this is an independent integration, not affiliated with or endorsed by CrewAI.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file onyx_gate_crewai-0.2.0.tar.gz.
File metadata
- Download URL: onyx_gate_crewai-0.2.0.tar.gz
- Upload date:
- Size: 26.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
96442f4e9cfcdcb35e27d1feff40317c57726a75812f0ed1ebd2363a99ef6e60
|
|
| MD5 |
4ea98e16d7af0e1cb9f8279db2c38c05
|
|
| BLAKE2b-256 |
4802b9a1fdfe7281952ccdf80a813b16edb44b2dde2d002e60db096dc4591e38
|
Provenance
The following attestation bundles were made for onyx_gate_crewai-0.2.0.tar.gz:
Publisher:
release.yml on theonyxfoundry/onyx-gate-crewai
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
onyx_gate_crewai-0.2.0.tar.gz -
Subject digest:
96442f4e9cfcdcb35e27d1feff40317c57726a75812f0ed1ebd2363a99ef6e60 - Sigstore transparency entry: 2702625624
- Sigstore integration time:
-
Permalink:
theonyxfoundry/onyx-gate-crewai@aeeb3645995b4613c5f5bda392e77bad3d9f7ee3 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/theonyxfoundry
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@aeeb3645995b4613c5f5bda392e77bad3d9f7ee3 -
Trigger Event:
push
-
Statement type:
File details
Details for the file onyx_gate_crewai-0.2.0-py3-none-any.whl.
File metadata
- Download URL: onyx_gate_crewai-0.2.0-py3-none-any.whl
- Upload date:
- Size: 20.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9134e64db6a9d2c5711cfec74b2b95e26a78d638d829c3fdc957450081bd8ff0
|
|
| MD5 |
b75d97b745fedd8a96cb9c76b5bb400d
|
|
| BLAKE2b-256 |
033c58b2b6e6b7cb929a5ccb47306ca14a07c9ff98f361e8eadaa452206cfc08
|
Provenance
The following attestation bundles were made for onyx_gate_crewai-0.2.0-py3-none-any.whl:
Publisher:
release.yml on theonyxfoundry/onyx-gate-crewai
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
onyx_gate_crewai-0.2.0-py3-none-any.whl -
Subject digest:
9134e64db6a9d2c5711cfec74b2b95e26a78d638d829c3fdc957450081bd8ff0 - Sigstore transparency entry: 2702625643
- Sigstore integration time:
-
Permalink:
theonyxfoundry/onyx-gate-crewai@aeeb3645995b4613c5f5bda392e77bad3d9f7ee3 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/theonyxfoundry
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@aeeb3645995b4613c5f5bda392e77bad3d9f7ee3 -
Trigger Event:
push
-
Statement type: