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 prompt runs through the injection firewall, PII redaction and any pluggable governance guards before it leaves Admina; the response is PII-redacted on the way back β streamed deltas through a 64-character windowed buffer β and every request writes one forensic record. Loop detection does not run on this surface.
HTTP 200 with a
synthetic completion whose finish_reason is content_filter. That is
deliberate, so OpenAI-compatible UIs render the refusal rather than erroring β which means
clients must detect blocks by finish_reason, not by status code. And Admina
forwards no credentials upstream, so ADMINA_GATEWAY_UPSTREAM (default
http://localhost:11434/v1, a local Ollama) must point at a keyless upstream such
as Ollama, a local vLLM server, or LM Studio.
See API Reference β OpenAI-compatible gateway for the full
request/response contract, and Configuration for the three
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)
Example
pip install "admina-framework[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-framework[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-framework as of v0.11.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.
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 and completion runs the full pipeline
(loop β firewall β PII β guards). 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-framework[openai]ADMINA_OPENAI_API_KEY / ADMINA_OPENAI_MODELrealadmina-framework[ollama]ADMINA_OLLAMA_HOST / ADMINA_OLLAMA_MODELrealadmina-framework[anthropic]ADMINA_ANTHROPIC_API_KEY / ADMINA_ANTHROPIC_MODELrealadmina-framework[mistral]ADMINA_MISTRAL_API_KEY / ADMINA_MISTRAL_MODELfallbackadmina-framework[bedrock]ADMINA_BEDROCK_REGION / ADMINA_BEDROCK_MODEL (standard AWS credential chain)fallbackadmina-framework[gemini]ADMINA_GEMINI_API_KEY / ADMINA_GEMINI_MODELfallbackadmina-framework[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 fallback is a documented interim state, not the intended end design: the
v0.11.0 release notes record the three fallback adapters as pending a follow-on, and the 0.11.0
design note targets native send_stream() implementations for them in the 0.11.x
line. 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.
pip install "admina-framework[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) β 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 runs when you pass a
session_id per call.