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+ (pinned in .python-version)
- 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 β proxy + infrastructure deps (what makes `admina dev` work) pip install "admina-framework[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 pip install "admina-framework[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) pip install "admina-framework[proxy-minimal]" # Optional: Rust-accelerated engine (opt-in since v0.9.4) pip install "admina-framework[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) pip install "admina-framework[presidio]" # Advanced β SDK only (embed governance in your own app; no local dev server) pip install admina-framework
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. After install, run:
python -m spacy download en_core_web_sm 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 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.
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>. 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.13.0-slim cosign verify ghcr.io/admina-org/admina-proxy:0.13.0 \ --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.13.0", # "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" # }, # "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 admina dev runs the in-memory forensic store. status
turns degraded while forensic records cannot be written. 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 of the generated compose
file receives no key, so its page signs in with the API key the same way. HTTP Basic
Auth is a feature of the repo's own compose file (the Docker-only workflow above),
where nginx asks for admin / ADMINA_DASHBOARD_PASSWORD and
presents the key to the proxy itself.
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)
The generated compose file publishes these ports on all interfaces and does not
publish ClickHouse. The repo's own docker-compose.yml differs: the
dashboard is on 127.0.0.1:3000 behind Basic Auth, ClickHouse on
127.0.0.1:8123, and the OTEL collector on localhost only (4317/4318, plus
8888/8889 Prometheus).
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. The compose file that admina dev --stack generates does not set it
at all, so the proxy uses its default, http://localhost:9000 β inside the
proxy container, where nothing listens. Point it 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)
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 3000, 3001, 8080, 4317 and 4318 on all interfaces.
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.