Quick Start
Get Admina running in under 5 minutes. The default workflow is
zero-Docker β a single uvicorn process serves both
the proxy API and the bundled dashboard on :3000 (auto-fallback to
the next free port if it is taken). Docker Compose remains available for the
full stack with ClickHouse, Grafana and OTEL collector.
- Python 3.11 to 3.14, all tested in CI (
.python-versionpins 3.11, the minimum, for the development environment) - Platforms β Linux x86_64 (Ubuntu 20.04+, Debian 11+, Fedora 38+, RHEL 9+) and macOS arm64 (12 Monterey+); both tested in CI
- Optional: Docker 24+ with Docker Compose v2 for the full stack
- 4 GB RAM available (the repo's full Docker Compose stack runs 8 containers)
Step 1 β Install Admina
# Recommended β the `admina` command on your PATH, with the proxy + infrastructure # deps that make `admina init` / `admina dev` work (uv's own tool environment) uv tool install "admina[proxy]" # with pip, in a virtual environment: pip install "admina[proxy]" # In a project β adds the dependency to its pyproject.toml (`uv init` creates one). # In an existing virtual environment: uv pip install "admina[X]"; with pip: pip install "admina[X]" uv add "admina[proxy]" # Broad install β [full] is [proxy,nlp,telemetry] and does not include Presidio; # the complete roll-up is [all] = [proxy,nlp,telemetry,adapters,presidio] since v0.11.0 uv add "admina[full]" # Gateway-only proxy (new in v0.13.0) β no Redis, ClickHouse, boto3, typer, # numpy or scikit-learn; serves the /v1 gateway only (see the note below) uv add "admina[proxy-minimal]" # Optional: Rust-accelerated engine (opt-in since v0.9.4) uv add "admina[rust]" # Optional: Microsoft Presidio as a selectable PII engine (new in v0.11.0 β # the default engine stays spacy-regex until you set ADMINA_PII_ENGINE=presidio) uv add "admina[presidio]" # Advanced β SDK only (embed governance in your own app; no local dev server) uv add admina
uv first (since v0.15.0). The docs and the install hints of Admina's own
error messages name uv
first and pip as the alternative; both install the same package. admina dev
starts uvicorn with the interpreter that runs the admina command, so it works
from the environment uv tool install creates. Upgrade with
uv tool upgrade admina, uv add --upgrade-package admina "admina[proxy]",
uv pip install -U "admina[proxy]" or pip install -U "admina[proxy]".
Package name (since v0.14.0). Admina is published on PyPI as
admina; up to 0.13.x the code was in admina-framework. The module
(import admina), the admina command, the extras and
admina-core keep their names. admina-framework 0.14.0 is a final
release that refuses to install and prints the commands to switch, so an unpinned
admina-framework requirement now fails (a pin such as
admina-framework<0.14 keeps installing 0.13). To upgrade an existing install,
remove both packages first, since both contain the admina module:
pip uninstall -y admina admina-framework && pip install "admina[proxy]"
β with uv, uv pip uninstall admina admina-framework && uv pip install "admina[proxy]",
or in a uv project uv remove admina-framework && uv add "admina[proxy]".
If import admina fails after you uninstalled admina-framework over a
new admina, run pip install --force-reinstall "admina[proxy]". Full
guide: package-name.md.
The default install runs the pure-Python governance engines. The Rust
accelerator is opt-in via the [rust] extra β
it pulls the admina-core wheel from PyPI, and
ADMINA_ENGINE=auto (the default) uses it for the firewall + loop
breaker while keeping PII on Python for full recall. The two engines are not
equivalent β the Python engines carry broader detection coverage, and the gap
is measured and tracked by the shipped red-team baseline rather than assumed.
Since v0.13.0, forcing ADMINA_ENGINE=rust without
admina-core installed is an error and the proxy does not start (it
used to warn and run the Python engines).
See the Rust engine guide.
[proxy-minimal] is gateway-only
The [proxy-minimal] extra (new in v0.13.0) is for an embedded
OpenAI-compatible gateway: start the proxy with
ADMINA_ENABLED_SURFACES=gateway and with REDIS_URL and
CLICKHOUSE_HOST empty. With the mcp or
integration surface enabled (the default enables every surface) and
neither [proxy] nor [rust] installed, the proxy refuses to
start and names the extra to install β so plain admina dev needs
[proxy]. Surfaces are described in the
Configuration reference.
The [nlp], [full], [presidio] and
[all] extras
require spaCy models, which PyPI does not allow as direct-URL dependencies.
Presidio supports English and Italian, and each language is active only if its
model is installed. The models are wheels on GitHub, not PyPI packages, and
python -m spacy download needs pip, which an environment created by uv does
not have β so with uv install the wheel itself. For the admina command of
uv tool install, name the extras and the model in the same command (extras
always go in that one command, e.g. "admina[proxy,openai]"):
# uv tool β for Presidio use "admina[proxy,presidio]" and add a second --with for it_core_news_sm uv tool install "admina[proxy,nlp]" --with "en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl" # virtual environment created by uv (in a uv project: uv add with the same "en-core-web-sm @ β¦" argument) uv pip install "https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl" uv pip install "https://github.com/explosion/spacy-models/releases/download/it_core_news_sm-3.8.0/it_core_news_sm-3.8.0-py3-none-any.whl" # Presidio, Italian # pip β one model per command python -m spacy download en_core_web_sm python -m spacy download it_core_news_sm
it_core_news_sm is only needed for the Presidio engine's Italian
support β the default spacy-regex engine uses
en_core_web_sm.
[proxy] boots without spaCy admina dev boots with the [proxy] extra only.
spaCy is imported lazily β without the [nlp] extra, PII redaction
runs in regex-only mode, still covering email, phone, SSN, IBAN,
IP address, credit card and EU national IDs. Add [nlp] when you
also want NER-based entities (person, organisation, location).
Step 2 β Initialize a project
admina (the banner and the commands), then
admina init demo --full-stack --no-pull and admina dev --no-browser,
each opening with the version line (O,O) Admina 0.15.2, up to
"Ready β http://localhost:3000".
The banner (since v0.15.2). admina without arguments prints a
two-line Heimdall banner β (O,O) Admina <version> over
/)_) governed AI development framework β and then the list of commands. In an
interactive terminal every command prints one line on standard error,
(O,O) Admina <version>; not with --help, --version
or --json, nor when standard error is not a terminal (pipes, CI). Set
ADMINA_NO_BANNER to any value to turn both off, and NO_COLOR to print
them without colours.
admina init my-project cd my-project # Scaffolds admina.yaml, docker-compose.yml, .env, and a runnable main.py
Step 3 β Start the proxy + dashboard
admina dev has three execution modes:
# 1. Default β zero-Docker local mode (one uvicorn serves API + dashboard) admina dev # 2. Full Compose stack (proxy + dashboard + redis + clickhouse + otel + grafana) admina dev --stack # 3. Stack + local AI infra (ollama + chromadb + open-webui) admina dev --with-llm # Expose on the LAN instead of localhost: admina dev --public # or --host 0.0.0.0
If you cloned the repo directly and prefer Docker Compose without the Python
CLI, bootstrap a .env with random credentials first:
git clone https://github.com/admina-org/admina.git cd admina ./scripts/bootstrap-secrets.sh # writes + prints random API key and dashboard password (.env) docker compose up --build # startup banner masks the key β re-read it with `cat .env`
This is the repo's own docker-compose.yml (8 containers, including a mock
MCP server and a mock agent), not the one admina init generates. Since
v0.13.0 it publishes the dashboard on 127.0.0.1:3000 only, behind HTTP
Basic Auth (admin / the generated password); nginx adds the API key only
to the dashboard's read-only routes. The proxy keeps its forensic records on the
named volume forensic-data, so they survive a re-created container, and
since v0.15.0 its last 100 EU AI Act assessments on the volume compliance-data
(ADMINA_EU_AI_ACT_ASSESSMENTS_PATH); the compose file that
admina init generates sets up the same volume. Bare admina dev does
not set the variable, so its assessments are kept in memory only.
Each release publishes ghcr.io/admina-org/admina-proxy:<version>
(proxy, NLP and telemetry extras plus the Rust engine and the bundled dashboard),
ghcr.io/admina-org/admina-proxy:<version>-slim (new in v0.13.0: the
[proxy] extra and the Rust engine, without NLP, telemetry or dashboard
files) and ghcr.io/admina-org/admina-dashboard:<version>, for amd64 and
arm64. Since v0.15.3 the proxy images run on python:3.14-slim (Python 3.14.9,
was 3.11) and are about 600 MB smaller: 1.09 GB full, 807 MB slim (were 1.72 GB and
1.24 GB). The
compose file that admina init generates pins the proxy and dashboard
images to your installed version. Since v0.13.0 the images ship with an SBOM and a
provenance attestation and are signed with cosign (keyless, GitHub OIDC):
docker pull ghcr.io/admina-org/admina-proxy:0.15.3-slim cosign verify ghcr.io/admina-org/admina-proxy:0.15.3 \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity-regexp '^https://github\.com/admina-org/admina/\.github/workflows/release-docker\.yml@'
latest moves only with a final release, and there is no
latest-slim tag β pin the version.
Step 4 β Verify governance is active
In local mode everything β API, dashboard and Swagger UI β is on
:3000. Under --stack the proxy is a separate container
on :8080, so swap the port in the commands below.
# Health check (always public) curl http://localhost:3000/health # { # "status": "healthy", # "service": "admina-proxy", # "version": "0.15.3", # "mode": "enforce", # "surfaces": ["gateway", "mcp", "integration", "compliance", "dashboard"], # "ruleset_sha256": "<64 hex>", # "forensic_writable": null, # "forensic_chain": null, # "engine": { # "engine": "python", # "rust_available": false, # "rust_version": null, # "selection": "auto", # "active": "python", # "pii_active": "python", # "firewall": "python", # "loop_breaker": "python", # "pii": "python" # }, # "sandbox": null, # "timestamp": "2026-01-01T12:00:00+00:00" # } # Check stats (auth required when ADMINA_API_KEY is set) curl http://localhost:3000/api/stats -H "X-API-Key: $ADMINA_API_KEY"
engine is an object, not a string. With the [rust] extra
installed it reports "active": "rust" while
"pii_active" stays "python" under the default
ADMINA_ENGINE=auto, which keeps full PII recall. Since v0.13.0,
firewall, loop_breaker and pii name the engines the
proxy actually built β an admina.yaml with custom firewall patterns keeps
firewall on python even when active is
rust. The other v0.13.0 fields: mode (governance mode),
surfaces, ruleset_sha256 (the active firewall ruleset), and
forensic_writable / forensic_chain, which are null
here because a fresh admina dev project runs the in-memory forensic store
(since v0.13.1 it uses FORENSIC_BACKEND or the backend in
admina.yaml when either sets one, and the startup banner says which). status
turns degraded while forensic records cannot be written. sandbox
(since v0.15.0) stays null until the
execution sandbox is turned on. Field-by-field
reference: API Reference.
Step 5 β Explore the dashboard
Since v0.12.1 the bundled dashboard asks for the API key once per
browser session: the sign-in form exchanges it for an HttpOnly,
SameSite=Strict session cookie, valid for one hour by default
(ADMINA_DASHBOARD_SESSION_TTL) and accepted only for the dashboard's read-only
API β /mcp, the /v1 gateway and the other APIs still need the key
itself. In an admina dev project, display the key with:
admina password show # API key + dashboard password from the project vault
Under admina dev --stack the dashboard container is set up as in the repo's
own compose file (since v0.13.1): it is published on 127.0.0.1:3000, nginx
asks for admin / ADMINA_DASHBOARD_PASSWORD (HTTP Basic Auth) and
presents the API key to the proxy itself. Both values are in the project vault, and
admina dev writes them to the generated .env.
Local mode β admina dev
http://localhost:3000 Governance dashboard β Admina Score, OISG adequacy, live event feed, EU AI Act gaps http://localhost:3000/docs Interactive API documentation (Swagger UI) β same process, same port Full stack β admina dev --stack
http://localhost:3000 Governance dashboard β served by nginx in the dashboard container http://localhost:8080 Proxy container β MCP proxy, admin API, /v1 gateway and /docs http://localhost:3001 Grafana β OTEL metrics and traces http://localhost:4317 OTEL collector, OTLP gRPC (4318 HTTP)
Since v0.13.1 the generated compose file publishes the dashboard and the OTEL collector
on 127.0.0.1 only (as are Ollama, ChromaDB and Open WebUI when the AI
infrastructure domain is on and the stack runs with --with-llm), the proxy (8080) and Grafana (3001) on all interfaces, and
does not publish ClickHouse. ClickHouse and Grafana have no default passwords:
docker compose stops unless CLICKHOUSE_PASSWORD and
GRAFANA_ADMIN_PASSWORD are set, which admina dev does. The repo's
own docker-compose.yml also publishes ClickHouse on
127.0.0.1:8123 and the collector's Prometheus ports 8888/8889.
Step 6 β Send your first governed request
Route your AI agent's MCP calls through Admina instead of directly to the MCP server.
In local mode the proxy listens on port 3000 (8080 under --stack).
/mcp forwards to UPSTREAM_MCP_URL, which local mode sets
empty. In the compose file that admina dev --stack generates it defaults to
http://host.docker.internal:9000 (since v0.13.1), an MCP server on port 9000
of your host; set UPSTREAM_MCP_URL to point it elsewhere. Point local mode at
your MCP server before this step, or use the repo's Docker-only workflow, whose compose
file wires the bundled mock MCP server for you.
/mcp is also authenticated, unlike /health.
# Send a test MCP call through the governance proxy curl -X POST http://localhost:3000/mcp \ -H "X-API-Key: $ADMINA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "read_file", "arguments": {"path": "/etc/hosts"} }, "id": 1 }' # The response includes governance metadata: # X-Admina-Governance-Action: ALLOW # X-Admina-Latency-Us: 6.25 # X-Admina-Event-Id: evt_01J... # X-Admina-Forensic-Hash: a3f8c9...
Step 7 β Point any OpenAI client at Admina
Since v0.11.0 the same proxy port also serves an OpenAI-compatible surface β
POST /v1/chat/completions and GET /v1/models β so any
client that speaks the OpenAI chat-completions API can be governed without a
code change. Point Admina at your model server first β these go in
.env or the shell that launches admina dev, since settings
are read once at start-up:
# Upstream OpenAI-compatible API Admina forwards to β must include the /v1 base path. # Default: http://localhost:11434/v1 (a local Ollama) export ADMINA_GATEWAY_UPSTREAM=http://localhost:11434/v1 # Optional (since v0.13.0) β API key sent upstream as "Authorization: Bearer"; # unset = no Authorization header (a keyless server such as Ollama or a local vLLM) export ADMINA_GATEWAY_UPSTREAM_API_KEY_FILE=/run/secrets/upstream_key # Optional β comma-separated model allow-list (default: empty = every model). # Filters GET /v1/models and, since v0.13.0, answers 403 model_not_allowed # to a chat completion for any other model export ADMINA_GATEWAY_MODELS_ALLOWLIST=llama3.1:8b # Optional β text returned to the caller when governance blocks a request # (default: "This request was blocked by the Admina governance policy.") export ADMINA_GATEWAY_BLOCK_MESSAGE="Blocked by policy."
Then talk to http://localhost:3000/v1 instead of the upstream
(http://localhost:8080/v1 under --stack). The
API key is your ADMINA_API_KEY β the proxy accepts it as
Authorization: Bearer or X-API-Key, so an OpenAI SDK's
api_key parameter works as-is.
curl -X POST http://localhost:3000/v1/chat/completions \ -H "Authorization: Bearer $ADMINA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "llama3.1:8b", "messages": [{"role": "user", "content": "Summarise this clauseβ¦"}] }'
from openai import OpenAI client = OpenAI( base_url="http://localhost:3000/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β¦"}], ) print(resp.choices[0].message.content)
Streaming works the same way. With PII redaction on (the default) each upstream
chunk is parsed and sent on with all of its fields, its text redacted across delta
boundaries (except logprobs and token_ids, sent as
null); with redaction off, the default passthrough stream mode
forwards the upstream bytes unchanged. data: [DONE] is sent when the
upstream sends it; an upstream or redaction failure mid-stream ends the stream with
one data: {"error": β¦} event instead:
for chunk in client.chat.completions.create( model="llama3.1:8b", messages=[{"role": "user", "content": "Summarise this clauseβ¦"}], stream=True, ): print(chunk.choices[0].delta.content or "", end="")
finish_reason
When governance blocks a prompt, the gateway returns HTTP 200
with a synthetic completion whose finish_reason is
content_filter (streaming: one chunk carrying the block message with
the same content_filter, then data: [DONE]). This is
deliberate β OpenAI-compatible UIs render the refusal instead of erroring β so by
default your client must detect blocks by finish_reason, not by status
code. Since v0.13.0 you can opt into ADMINA_GATEWAY_BLOCK_STATUS=403 for an
OpenAI-style governance_blocked error instead, and every chat completion
response carries X-Admina-Action (ALLOW or
BLOCK) once the request has an event id. Upstream errors (4xx, 5xx) now
reach the client with their own status. The client's own credentials are never
forwarded upstream; the only key the upstream sees is the one you configure above.
Named upstream routes, timeouts and the full contract:
Gateway reference.
What just happened?
Every call you made was:
- Scanned for PII in both directions (Data Sovereignty)
- Checked for prompt injection patterns (Agent Security β Firewall)
- Checked for infinite loop signatures (Agent Security β Loop Breaker)
- Added to the SHA-256 hash chain audit log (Compliance β Forensic)
- Classified for EU AI Act risk level (Compliance)
- Recorded as an OpenTelemetry span (Compliance β OTEL) β only with the
[telemetry]extra installed andOTEL_ENDPOINTset, which--stackdoes for you; bareadmina devclears it - Counted into the Admina Score at
/api/dashboard/scoreβ the live runtime composite (residency, audit coverage, EU AI Act coverage, recent attacks, forensic chain; since v0.15.0 the EU AI Act component is left out, and the score rescaled, when the latest assessment isNOT_APPLICABLE)
One exception on the /v1 gateway from Step 7: loop detection does not run
on that surface. Since v0.13.0 each gateway chat completion writes two forensic
records, gateway_request and gateway_response (count
gateway_request to count requests), and, with OpenTelemetry on, emits a
gateway.chat.completions span.
Your OISG score at /api/dashboard/oisg is a
static capability assessment β 4 pillars Γ 5 criteria Γ 5 points,
each criterion satisfied when the corresponding subsystem is present and configured.
It does not move with traffic: a stock instance reads the same after one request as
after a million. Nor does it start at the top β a bare admina dev leaves
five criteria unsatisfied (o1 and i3 want AI infra + RAG enabled, o5 and s5 want the
Rust engine, g3 wants a governance-guard plugin), capping it at 75 in the
Good coverage band until you enable them.
See OISG Adequacy for the full paradigm.
Next steps
Troubleshooting
First stop: admina doctor
The CLI ships with a diagnostic command that inspects your environment and reports
which checks pass or fail β Python version, Rust engine availability, plugin discovery,
admina.yaml validity, and infrastructure reachability.
admina doctor Containers fail to start
Check that all required ports are free. The compose file generated for
admina dev --stack binds 3001 and 8080 on all interfaces, plus 3000, 4317 and
4318 on localhost only (since v0.13.1).
The repo's own docker-compose.yml binds 3001 and 8080 on all interfaces,
plus 3000, 8123, 4317, 4318, 8888 and 8889 on localhost only.
Run docker compose logs proxy for detailed error output. With
ADMINA_API_KEY set, the repo's dashboard container also refuses to start
without ADMINA_DASHBOARD_PASSWORD β run
./scripts/bootstrap-secrets.sh.
Rust engine not detected
Under the default ADMINA_ENGINE=auto, Admina uses the Python engines when
admina-core is not installed: the health endpoint's engine
object shows "rust_available": false and "active": "python".
With ADMINA_ENGINE=rust the same situation is, since v0.13.0, an
EngineSelectionError and the proxy does not start. Install the
[rust] extra, or build admina-core for your platform (see the
Rust engine guide).
The proxy stops at startup after upgrading to 0.13
v0.13.0 turns several silent fallbacks into startup errors that name the cause:
ADMINA_ENGINE=rust without admina-core or with Python-only
firewall keys in admina.yaml, a value of the wrong type in
admina.yaml, a file named by ADMINA_CONFIG that is missing or
invalid, and an unknown name in ADMINA_ENABLED_SURFACES. A
FORENSIC_BACKEND=filesystem without a usable directory (or s3
without boto3 or S3) no longer falls back to the in-memory store: the proxy starts
recording nothing, with /health reporting "status": "degraded",
or does not start with ADMINA_FORENSIC_FAIL_MODE=closed. Also check
heuristic_threshold: projects generated by admina init up to 0.12
set 0.7, which the firewall now applies β set 0.5 to keep the
previous behaviour. The repo's
upgrade guide
lists every change in the order to check it.
Need help?
Open an issue on GitHub or start a discussion in GitHub Discussions.