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.

Prerequisites
  • 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.

Extra step for the NLP and Presidio extras

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
Without the CLI? Docker-only workflow

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.

Prebuilt, signed images

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="")
Blocks are HTTP 200 by default β€” check 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 and OTEL_ENDPOINT set, which --stack does for you; bare admina dev clears 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.

OISG measures capability, not traffic

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.