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.

Sidecar or in-process. OpenClaw, n8n and Cheshire Cat AI talk to a running Admina proxy and are pointed at it with a URL. The LangChain and CrewAI callbacks run the governance engines in-process: there is no proxy to start and no URL to configure. The model adapters are likewise in-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.

What changed in v0.13.0. 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.
Governance guards must be thread-safe. Because the gateway runs guards in its worker threads (up to 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 governance
  • shell_exec β€” Shell command execution
  • file_write β€” File system write operations
  • http_request β€” Outbound HTTP requests
  • message_send β€” Agent-to-agent or agent-to-user messages
v0.11.0 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 model
  • on_llm_end β€” Scans completions for PII
  • on_tool_start β€” Firewall, loop and PII check before tool execution
  • on_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-generated langchain-… id)
  • pii_redaction β€” Enable PII redaction, Data Sovereignty domain (default True)
  • firewall β€” Enable the injection firewall, Agent Security domain (default True)
  • loop_detection β€” Enable the loop breaker, Agent Security domain (default True)
  • on_block β€” Behaviour when a request is blocked: "raise" (default) raises GovernanceBlockedError, "warn" logs and continues
  • audit β€” Emit events to the governance event bus (default True)

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") and audit.
  • AdminaTaskCallback β€” Redacts and audits completed task outputs. Parameters: session_id, pii_redaction and audit only β€” 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).
Breaking in v0.11.0 β€” 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.
n8n community nodes: After installing, restart your n8n instance. There is no dedicated Admina category β€” the nodes use standard n8n groups, so search the palette for "Admina": Admina Govern is a 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.
Breaking in v0.11.0 β€” 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.

Packaging note. The PyPI distribution 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 language
  • detect_pii β€” guardrails.hub.DetectPII: personally identifiable information
  • detect_jailbreak β€” guardrails.hub.DetectJailbreak: prompt injection and jailbreak attempts
  • bias_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).

Local-only inference, enforced. 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:

ProviderInstallKey env varsStreaming
OpenAIadmina[openai]ADMINA_OPENAI_API_KEY / ADMINA_OPENAI_MODELreal
Ollama (local)admina[ollama]ADMINA_OLLAMA_HOST / ADMINA_OLLAMA_MODELreal
Anthropicadmina[anthropic]ADMINA_ANTHROPIC_API_KEY / ADMINA_ANTHROPIC_MODELreal
Mistraladmina[mistral]ADMINA_MISTRAL_API_KEY / ADMINA_MISTRAL_MODELfallback
AWS Bedrockadmina[bedrock]ADMINA_BEDROCK_REGION / ADMINA_BEDROCK_MODEL (standard AWS credential chain)fallback
Google Geminiadmina[gemini]ADMINA_GEMINI_API_KEY / ADMINA_GEMINI_MODELfallback
vLLM (local, OpenAI-compatible)admina[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}
How the streaming redactor works. 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
Full governance by default (v0.10.0). 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.
Value-only PII redaction (v0.13.0). PII redaction now reads text values and keeps the structure around them: for 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.