Integrations
Admina integrates with the most popular AI agent frameworks and orchestration tools, in two shapes: sidecar integrations that route traffic through the Admina proxy over HTTP, and in-process integrations that call the governance engines directly inside your own Python process.
GovernedModel runs the pipeline
around them β while the GuardrailsAI guard is a plugin loaded by the proxy itself.
OpenAI-compatible gateway
Since v0.11.0 the proxy exposes an OpenAI-compatible HTTP surface β
POST /v1/chat/completions (streaming and non-streaming) and
GET /v1/models β on the same port 8080 as /mcp. Any client,
library or front-end that speaks the OpenAI chat-completions API can be pointed at
Admina with no code change: set the base URL to http://localhost:8080/v1
and the API key to your ADMINA_API_KEY. Port 8080 is the proxy port under
admina dev --stack; in the default zero-Docker local mode
(admina dev) the same surface is on :3000. See
Quick Start.
from openai import OpenAI client = OpenAI( base_url="http://localhost:8080/v1", # Admina, not the provider api_key="<your ADMINA_API_KEY>", ) resp = client.chat.completions.create( model="llama3.1:8b", messages=[{"role": "user", "content": "Summarise this clauseβ¦"}], )
The request runs through the injection firewall, PII redaction, the egress stage and any
pluggable governance guards before it leaves Admina β since v0.13.0 the firewall scans
every string of the request body (tool definitions, response_format and tool
call arguments included), and the pipeline runs in worker threads off the event loop. The
completion is PII-redacted on the way back β streamed text through a 64-character windowed
buffer per choice and field. Loop detection does not run on this surface. Each chat
completion writes two forensic records, gateway_request and
gateway_response.
ADMINA_GATEWAY_UPSTREAM (default http://localhost:11434/v1, a local
Ollama) no longer has to be keyless: an upstream API key
(ADMINA_GATEWAY_UPSTREAM_API_KEY or _FILE, overridable per route) is
sent as Authorization: Bearer, and named upstream routes are selected per request
with X-Admina-Upstream. The caller's own credentials are never forwarded. With PII
redaction off, streams pass through byte for byte; with it on, every upstream chunk is sent on
with all of its fields, its text redacted (logprobs and token_ids are
sent as null). Upstream 4xx/5xx errors reach the client with their
status. ADMINA_GATEWAY_MODELS_ALLOWLIST now also answers 403
(model_not_allowed) to a chat completion for an unlisted model. A block is still
HTTP 200 with finish_reason: "content_filter" by default β so
OpenAI-compatible UIs render the refusal β unless you set
ADMINA_GATEWAY_BLOCK_STATUS=403; either way the response carries
X-Admina-Action: BLOCK.
ADMINA_GATEWAY_PIPELINE_WORKERS at once, each call on
its thread's own event loop), one guard instance can be called concurrently. Per the
BaseGovernanceGuard contract a guard must protect shared mutable state with a
threading.Lock (or keep it per thread) and must not keep loop-bound objects β
an asyncio.Lock, an asyncio.Queue, a pooled
httpx.AsyncClient β across calls.
See API Reference β OpenAI-compatible gateway for the full
request/response contract (headers, error codes, routes, limits), and
Configuration for the ADMINA_GATEWAY_*
variables.
OpenClaw
Admina ships with a ready-to-use OpenClaw skill that routes all MCP tool calls from your agent through the Admina governance proxy. The setup steps below install the skill and route the agent's traffic via Admina.
How it works
The admina-governance SKILL.md routes all agent actions through
/api/v1/validate and /api/v1/audit. Governed action types include:
llm_callβ LLM prompt and completion governanceshell_execβ Shell command executionfile_writeβ File system write operationshttp_requestβ Outbound HTTP requestsmessage_sendβ Agent-to-agent or agent-to-user messages
MODIFY β REDACT: no change needed here.
The /api/v1/validate action value returned when a request is allowed
but PII was masked changed from "MODIFY" to "REDACT" in v0.11.0. The
in-repo admina-governance SKILL.md contains no occurrence of either string: it
instructs the agent to branch on other response fields, not on action. A pinned
pre-0.11 copy of the skill is therefore not broken by this particular rename. Any
custom OpenClaw tooling you wrote around /api/v1/validate that branches on
action == "MODIFY" does have to change β the current response fields are
action, risk_level, checks,
redacted_content and latency_ms; see the
API Reference.
Skill install
The skill lives at admina/integrations/openclaw/admina-governance/ and ships
exactly three files: SKILL.md (the skill itself), admina.yaml
(the sidecar's config) and setup.sh. Copy the directory into your OpenClaw
skills directory, then run setup.sh: it starts the Admina proxy as a Docker
container named admina-openclaw-sidecar on port 18790
(override with ADMINA_PORT), mounts the adjacent admina.yaml
read-only at /app/admina.yaml, and waits for /health. The script
then prints the ADMINA_PROXY_URL to export β the skill reads that variable for
every /api/v1/validate and /api/v1/audit call, and refuses to act
if the sidecar is unreachable.
# Start the Admina governance sidecar cd admina/integrations/openclaw/admina-governance chmod +x setup.sh ./setup.sh # Point the skill at the sidecar export ADMINA_PROXY_URL=http://127.0.0.1:18790 # Health check and teardown ./setup.sh --status ./setup.sh --uninstall
LangChain
The AdminaCallbackHandler is a drop-in callback for ChatOpenAI
and any other LangChain model. It governs all LLM and tool events without changing your
existing chain logic. It runs in-process β the firewall, PII redactor and
loop breaker are called directly, so there is no proxy to start and no URL or API key to
configure.
Governed events
on_llm_startβ Validates prompts before they reach the modelon_llm_endβ Scans completions for PIIon_tool_startβ Firewall, loop and PII check before tool executionon_tool_endβ PII scan and audit trail of tool results
Configuration
The handler accepts the following parameters:
session_idβ Session identifier for loop detection across calls (default: an auto-generatedlangchain-β¦id)pii_redactionβ Enable PII redaction, Data Sovereignty domain (defaultTrue)firewallβ Enable the injection firewall, Agent Security domain (defaultTrue)loop_detectionβ Enable the loop breaker, Agent Security domain (defaultTrue)on_blockβ Behaviour when a request is blocked:"raise"(default) raisesGovernanceBlockedError,"warn"logs and continuesauditβ Emit events to the governance event bus (defaultTrue)
loop_detection is on by default, and the Python loop breaker needs numpy and
scikit-learn, which ship with [proxy], not [nlp] β hence
[proxy,nlp] below. Alternatives: [rust] for the Rust loop breaker
(which also moves the firewall to Rust under ADMINA_ENGINE=auto), or
loop_detection=False. Without either package the first governed call raises an
ImportError naming the extra. The same applies to the CrewAI step callback.
Example
pip install "admina[proxy,nlp]" langchain langchain-openai from admina.integrations.langchain.callbacks import AdminaCallbackHandler from langchain_openai import ChatOpenAI handler = AdminaCallbackHandler( session_id="my-session", pii_redaction=True, firewall=True, ) llm = ChatOpenAI(callbacks=[handler]) # Every call to llm.invoke() is now governed response = llm.invoke("Summarize this document") # handler.last_result β GovernanceResult for the most recent check # handler.get_stats() β session_id, call_count, block_count, redact_count, features
CrewAI
Admina provides two callbacks for CrewAI:
AdminaStepCallback fires after each agent step, and
AdminaTaskCallback fires after task completion.
Both are callables you instantiate per crew, and both run
in-process β no proxy URL, no API key.
Callbacks
-
AdminaStepCallbackβ Governs each agent step (tool calls, LLM calls, reasoning) with the firewall, PII redactor and loop breaker. Parameters:session_id,pii_redaction,firewall,loop_detection,on_block("raise"or"warn") andaudit. -
AdminaTaskCallbackβ Redacts and audits completed task outputs. Parameters:session_id,pii_redactionandauditonly β it does not run the firewall or the loop breaker.
Example
pip install "admina[proxy,nlp]" crewai from admina.integrations.crewai.callbacks import AdminaStepCallback, AdminaTaskCallback from crewai import Crew, Agent, Task researcher = Agent( role="Researcher", goal="Analyze market trends", backstory="Senior market analyst", ) task = Task( description="Research Q4 revenue for ACME Corp", expected_output="Revenue summary", agent=researcher, ) crew = Crew( agents=[researcher], tasks=[task], step_callback=AdminaStepCallback(session_id="my-crew"), task_callback=AdminaTaskCallback(session_id="my-crew"), ) # Kick off β every step and task is governed result = crew.kickoff()
The module also exports two ready-made instances with default settings,
admina_step_callback and admina_task_callback, if you do not need
to configure anything: Agent(step_callback=admina_step_callback) and
Crew(task_callback=admina_task_callback).
n8n
The n8n-nodes-admina package adds three governance nodes to your
n8n workflows:
Install
npm install n8n-nodes-admina Nodes
- AdminaGovern β Inline governance node. Place it before any AI node in your workflow. Validates inputs, blocks non-compliant requests, and adds governance metadata to the output.
- AdminaAudit β Passive logging node. Place it after any AI node to log all requests and responses to the Admina audit trail without blocking.
- AdminaDashboard β WebSocket trigger node. Streams real-time governance events from Admina into n8n, enabling reactive workflows (alerts, Slack notifications, compliance reports).
MODIFY is now REDACT.
The /api/v1/validate response action value returned when a request
is allowed but PII was masked changed from "MODIFY" to "REDACT",
with no compatibility shim. The in-repo AdminaGovern node and the Cheshire Cat AI
plugin were updated in the same release, so update your installed copies. Neither package
version was bumped (both are still 1.0.0), so there is no manifest signal β any downstream n8n
expression comparing json._admina.action to 'MODIFY' must be changed
to 'REDACT'. Node parameters (contentField, onBlock,
logToForensic) and the Cheshire Cat plugin settings are unchanged; see the
API Reference. Admina is pre-1.0 β the public API is feature-complete
and production-ready, but the stability commitment is deferred to 1.0, which is what permits a
rename in a minor release.
transform node, Admina Audit an
output node, and Admina Dashboard a trigger.
Cheshire Cat AI
Admina integrates with Cheshire Cat AI as a Python plugin. The plugin hooks into the Cat's event system to govern agent actions at three critical points.
Governed hooks
-
agent_fast_replyβ Intercepts the agent's reply before it is sent to the user. Enables PII redaction, toxicity filtering, and compliance checks on outputs. -
before_cat_sends_messageβ Final checkpoint before the message leaves the Cat. Applied after all other plugins have processed the response. -
before_cat_recalls_memoriesβ Governs memory recall to prevent leaking sensitive data from the vector store.
MODIFY is now REDACT.
All three hooks in admina_governance.py branch on
result.get("action") == "REDACT" before substituting
redacted_content; before v0.11.0 the same three branches compared against
"MODIFY". A pinned pre-0.11 copy of the plugin therefore fails open against
a v0.11 proxy: the redacted branch never fires, so the un-redacted user message is left in
working memory, the un-redacted reply is sent to the user, and the un-redacted text is used as
the RAG query. Blocking is unaffected β the BLOCK branches are untouched, and only
the PII-redaction path silently stops applying. The plugin version was not bumped (still
1.0.0 in plugin.json), so there is no manifest signal: copy the
v0.11.0 plugin over your installed one. Plugin settings are unchanged.
Install
The plugin lives at admina/integrations/cheshirecat/admina-plugin/. Its
setup.sh starts the Admina proxy as a Docker sidecar on port
18790; copy the plugin folder into your Cat, point it at the sidecar with the
ADMINA_PROXY_URL environment variable, and enable it from the admin panel.
The plugin reads its configuration from the environment β ADMINA_PROXY_URL
(default http://localhost:18790) and ADMINA_TIMEOUT in seconds
(default 5) β not from a settings file.
# Start the Admina governance sidecar cd admina/integrations/cheshirecat ./admina-plugin/setup.sh # Copy the plugin into your Cat's plugins directory cp -r admina-plugin/ <your-cheshire-cat>/plugins/admina-plugin/ # In the Cat's .env or docker-compose.yml ADMINA_PROXY_URL=http://host.docker.internal:18790
GuardrailsAI
Admina ships GuardrailsAIGuard, a built-in governance guard
(admina.plugins.builtin.guards.guardrailsai_guard, registered under the name
guardrailsai) that wraps GuardrailsAI
validators as a stage of Admina's own pipeline. You do not construct it yourself β the proxy
discovers every built-in guard at startup and builds it with the matching
plugin_config block from admina.yaml.
All inference runs locally β no data leaves your infrastructure.
guardrails-ai is
currently in quarantine, so the [guardrailsai] extra is still not
published with admina (unchanged through v0.14.0). The plugin itself remains
in the codebase and is auto-detected at runtime if you install the
guardrails-ai distribution (imported as guardrails) separately,
e.g. from a local wheel or mirror.
Install
# From a local wheel or mirror β PyPI itself will not serve it while # the distribution is quarantined. Admina auto-detects it on startup. pip install ./guardrails_ai-<version>-py3-none-any.whl
Wrapped validators
Four validator names are mapped to guardrails.hub classes; any other name raises
ValueError. Extra keys on a validator entry are passed straight to the
GuardrailsAI validator constructor, and on_fail defaults to "noop"
so Admina β not GuardrailsAI β decides the action. Set on_fail explicitly on a
validator entry and your value wins, which hands the action back to GuardrailsAI.
toxic_languageβguardrails.hub.ToxicLanguage: toxic or harmful languagedetect_piiβguardrails.hub.DetectPII: personally identifiable informationdetect_jailbreakβguardrails.hub.DetectJailbreak: prompt injection and jailbreak attemptsbias_checkβguardrails.hub.BiasCheck: biased content
Configuration
The guard is wired in the top-level plugin_config map of
admina.yaml, keyed by the plugin name:
plugin_config:
guardrailsai:
inference_mode: local # local only β see below
validators:
- name: toxic_language
threshold: 0.5
- name: detect_pii
entities: ["EMAIL_ADDRESS", "PHONE_NUMBER", "IBAN"]
- name: detect_jailbreak
threshold: 0.8
There is no enabled switch: the guard is instantiated whenever the
guardrails package imports successfully, and skipped with a warning when it does
not. Note that admina.yaml.example also carries a
domains.agent_security.domains.guardrailsai block β the config loader does not
read it, so put your settings under plugin_config.
At request time the guard runs every configured validator over the payload's
content field and returns action: "ALLOW" with
risk_level: "LOW" when validation passes, or action: "BLOCK" with
risk_level: "HIGH", the validator error string in details and the
class names of GuardrailsAI's failed-validation entries in metadata.failed when it
does not. Empty content
is allowed without running anything. The guard blocks or allows β it never redacts.
Since v0.13.0 it runs one validation at a time: the shared guardrails.Guard
and its validators are used under a lock, because the gateway now calls guards from
several worker threads at once (see the gateway section).
inference_mode accepts
local only, and it is the default. Anything else β including
remote β makes the constructor raise ValueError at startup rather
than sending prompts outside the deployment perimeter. There is no environment variable that
relaxes this.
Model provider adapters
Where the framework integrations above govern agent traffic, model adapters let
the SDK govern direct calls to an LLM provider: wrap a provider in
GovernedModel and every prompt runs the full pipeline
(loop β firewall β PII β egress β guards), with PII redaction applied to the completion. The
loop stage runs only with loop_detection=True and a per-call
session_id; the egress stage runs unless agent_security.egress.surfaces
leaves out sdk (since v0.13.0). Each adapter is a built-in plugin that
lazy-imports its provider SDK, so the dependency is only required when
the adapter is actually used. Since v0.10.0 there are seven, installed via per-provider extras:
admina[openai]ADMINA_OPENAI_API_KEY / ADMINA_OPENAI_MODELrealadmina[ollama]ADMINA_OLLAMA_HOST / ADMINA_OLLAMA_MODELrealadmina[anthropic]ADMINA_ANTHROPIC_API_KEY / ADMINA_ANTHROPIC_MODELrealadmina[mistral]ADMINA_MISTRAL_API_KEY / ADMINA_MISTRAL_MODELfallbackadmina[bedrock]ADMINA_BEDROCK_REGION / ADMINA_BEDROCK_MODEL (standard AWS credential chain)fallbackadmina[gemini]ADMINA_GEMINI_API_KEY / ADMINA_GEMINI_MODELfallbackadmina[adapters]ADMINA_VLLM_BASE_URL / ADMINA_VLLM_MODEL (model required)real
New in v0.11.0: real means the adapter implements
BaseModelAdapter.send_stream() against the provider's own streaming API, so you
get incremental deltas. fallback means it inherits the base implementation, which
awaits the full response and yields it as a single chunk β functionally correct, but with no
latency benefit. The v0.11.0 release notes recorded the three fallback adapters as pending a
follow-on with native send_stream() implementations; as of v0.13.0 they are still
on the fallback. Governance is identical either way β a single-chunk response still runs through
the same redactor β so only the delta granularity changes when they land.
Two roll-up extras bundle them: [adapters] pulls every provider, and
[all] is [proxy,nlp,telemetry,adapters,presidio] for a complete
install. The older [full] extra is [proxy,nlp,telemetry] and does
not include Presidio.
Fixed in v0.13.0: GovernedModel.ask() and stream() work with the
bare SDK install (pip install admina plus a provider extra). Earlier
releases built the Python loop breaker on every call and failed with
ModuleNotFoundError: numpy; it is now built only when loop detection runs, and
then needs [proxy] (numpy and scikit-learn) or [rust].
pip install "admina[anthropic]" from admina.plugins.builtin.adapters.anthropic import AnthropicAdapter from admina.sdk import GovernedModel # The Anthropic call now runs through the full governance pipeline model = GovernedModel(model_name="claude-sonnet-4-5", adapter=AnthropicAdapter()) resp = await model.ask("Summarise this contract clauseβ¦") # resp.action β "ALLOW" | "REDACT" | "BLOCK" (firewall + guards run by default since v0.10.0)
Streaming (v0.11.0)
GovernedModel.stream(prompt, **kwargs) is an async generator consumed with
async for. It yields plain str deltas, already PII-redacted, and is
async-only β there is no stream_sync(). Three kwargs are intercepted by
stream() itself: context (the system prompt, handed to the adapter
as its own context argument), stream_window_chars (default 64) and
session_id (enables loop detection when the model was built with
loop_detection=True) β the last two are never passed on;
everything else is forwarded to the provider. Once the iterator is fully
exhausted, model.last_stream_result is a plain dict β not a dataclass,
so access it with ["action"], never .action β with exactly eight
keys: action, pii_count, model,
input_tokens, output_tokens, finish_reason,
time_to_first_token_ms and duration_ms. input_tokens
and output_tokens are always None: there is no token accounting on
the streaming path. action is "ALLOW", "REDACT" (PII
was found and masked) or "BLOCK". A blocked prompt yields zero deltas
and returns normally β no exception is raised β with
last_stream_result["action"] == "BLOCK" and finish_reason of
content_filter.
from admina.plugins.builtin.adapters.ollama import OllamaAdapter from admina.sdk import GovernedModel model = GovernedModel(model_name="llama3.1:8b", adapter=OllamaAdapter()) async for delta in model.stream("Summarise this contract clauseβ¦"): print(delta, end="", flush=True) # Read the outcome AFTER the iterator is exhausted print(model.last_stream_result) # β {'action': 'REDACT', 'pii_count': 1, 'model': 'llama3.1:8b', # 'input_tokens': None, 'output_tokens': None, 'finish_reason': 'stop', # 'time_to_first_token_ms': 41.2, 'duration_ms': 812.5}
StreamRedactor holds back a
trailing window of stream_window_chars characters (default 64) so a PII entity
split across two deltas is still masked. Two consequences: emission lags by about one window,
and a response shorter than the window arrives as a single delta at the end. The window must
also exceed the longest expected entity β an entity longer than
stream_window_chars can be emitted before it is fully visible, and therefore
un-redacted. Note too that RetryPolicy is not applied to .stream()
(it only wraps ask()), that last_stream_result is only written when
the iterator runs to completion β breaking out of the loop early leaves the previous value β
and that there is no mid-stream block: once deltas start flowing, content is redacted, never
halted.
StreamRedactor on your own stream
StreamRedactor is a public export β from admina.sdk import StreamRedactor
β and is pure text-in/text-out with no I/O, so you can drive it over any stream you already own
(a provider SDK you call directly, an SSE relay, a websocket) without going through
GovernedModel. Its constructor is
StreamRedactor(pii_redactor, window_chars=64): the first argument is any object
implementing the PIIBridge protocol β i.e. exposing
redact(text) returning a dict with redacted_text and
count β and admina.engines.get_pii_engine() returns the configured one.
window_chars below 1 raises ValueError.
Mind the parameter name. The constructor kwarg is window_chars;
the GovernedModel.stream() kwarg for the same setting is
stream_window_chars. They are not interchangeable, and both mistakes are quiet:
StreamRedactor(engine, stream_window_chars=128) raises TypeError, while
model.stream(prompt, window_chars=128) never reaches the redactor at all β
stream() pops only stream_window_chars, so window_chars
falls through into the kwargs handed to the adapter's send_stream() as an ordinary
provider request parameter and the window stays at 64. What happens next is the provider SDK's
business: it may reject the unknown parameter or ignore it, but either way the window was never
changed.
Two methods drive it. feed(delta) takes one raw delta and returns a
list[str] of 0 or more redacted, safe-to-emit deltas β the list is empty while the
buffer is still inside the window, or while an entity straddles the emit boundary.
finish() flushes the held window and returns a
(final_redacted_tail, summary) tuple, where summary is
{"pii_count": N} for the whole stream. Calling finish()
is mandatory: skip it and the last window_chars characters of the response are
never emitted.
from admina.engines import get_pii_engine from admina.sdk import StreamRedactor redactor = StreamRedactor(get_pii_engine(), window_chars=64) for raw in my_provider_stream: # any iterable of raw str deltas for safe in redactor.feed(raw): # 0+ redacted deltas per call print(safe, end="", flush=True) tail, summary = redactor.finish() # always flush at end of stream print(tail, end="") print(summary["pii_count"]) # entities masked across the stream
GovernedModel.ask() now runs
the injection firewall and any pluggable guards on the prompt β not just PII redaction β and can
return action="BLOCK" with empty text. Opt out per stage with
GovernedModel(firewall_enabled=False, β¦). Loop detection is off by default on
GovernedModel: it runs only with GovernedModel(loop_detection=True, β¦)
and a session_id passed per call.
GovernedAgent (and /mcp tool
parameters and results) the values of a dict are redacted and its keys, ids and tool names
are left as they are. Pass GovernedAgent(upstream, redact_keys=True) to redact
dict keys too. Unlike GovernedModel, GovernedAgent keeps
loop_detection=True by default, so it needs [proxy] or
[rust] for the loop breaker.