API Reference

Admina exposes a REST + JSON-RPC 2.0 API β€” on :3000 in the default zero-Docker local mode, on :8080 under --stack. The interactive Swagger UI is served at /docs on that same port unless ADMINA_API_DOCS_ENABLED=false.

Every example below uses :8080, the proxy port under admina dev --stack. In the default zero-Docker local mode (admina dev) the same API β€” and /docs β€” is served by a single process on :3000; swap the port accordingly. See Quick Start.

Authentication: Every endpoint requires the X-API-Key header (or Authorization: Bearer <key>) when ADMINA_API_KEY (or ADMINA_API_KEY_FILE) is set, except for the exempt paths: /health, /metrics, /docs, /openapi.json, /redoc and the bundled dashboard shell (/, /heimdall.png, /vendor/*). The gateway routes under /v1 are covered by the same check. With no key and no auth provider configured every other request gets 401 unless ALLOW_UNAUTHENTICATED=true. The settings below change what is exempt or served; their reference is on Configuration.
ADMINA_METRICS_REQUIRE_AUTHSince v0.13.0. true puts /metrics behind the API key (default false)
ADMINA_API_DOCS_REQUIRE_AUTHSince v0.13.0. true puts /docs, /redoc and /openapi.json behind the API key
ADMINA_API_DOCS_ENABLEDSince v0.12.1. false stops serving the API docs: those paths answer 404
ADMINA_DASHBOARD_ENABLEDSince v0.12.1. false (or dashboard.enabled: false in admina.yaml) stops serving the dashboard shell and its sign-in route; the /api/dashboard/* data API stays available with the API key
ADMINA_ENABLED_SURFACESSince v0.13.0. Comma-separated surfaces to serve (empty = all): gateway (/v1/*), mcp (/mcp, /mcp/*), integration (/api/v1/*), compliance (/api/compliance/*), dashboard (/api/dashboard/*, /api/stats, /api/events, the shell). A disabled surface answers 404 before authentication; /health and /metrics are always served
ADMINA_MAX_REQUEST_BYTESSince v0.13.0. Body cap on every route, default 10 MiB (0 = no limit). A larger body gets 413 before authentication and before it is parsed β€” {"detail": "Request body too large"}, or the OpenAI error format (code request_too_large) under /v1
ADMINA_AUDIT_APPEND_KEYSince v0.13.0. A second key accepted by POST /api/v1/audit only; every other route refuses it

Dashboard session (since v0.12.1). The bundled dashboard exchanges the API key for a signed, HttpOnly, SameSite=Strict cookie, admina_dashboard_session, scoped to /api/ (see POST /api/dashboard/session). The cookie authenticates only GET / HEAD requests to /api/dashboard/* and /api/stats, and the live-feed WebSocket; /mcp, /v1/*, /api/v1/*, /api/compliance/* and /api/events ignore it and need the API key. The ?api_key= query parameter is accepted only on the dashboard WebSocket upgrade, never on HTTP requests, and is deprecated since v0.13.0: the first connection that uses it logs a warning (once per process).

Core endpoints

GET /health public

Health check. Always public and always served, whatever ADMINA_ENABLED_SURFACES says.

curl http://localhost:8080/health
# Response (filesystem forensic backend, Rust engine)
{
  "status": "healthy",
  "service": "admina-proxy",
  "version": "0.13.0",
  "mode": "enforce",
  "surfaces": ["gateway", "mcp", "integration", "compliance", "dashboard"],
  "ruleset_sha256": "<64 hex>",
  "forensic_writable": true,
  "forensic_chain": "ok",
  "engine": {
    "engine": "rust",
    "rust_available": true,
    "rust_version": "0.13.0",
    "selection": "auto",
    "active": "rust",
    "pii_active": "python",
    "firewall": "rust",
    "loop_breaker": "rust",
    "pii": "python"
  },
  "timestamp": "2026-10-05T09:30:00+00:00"
}
statushealthy, or degraded while forensic records cannot be written: forensic_writable is false, the last record or chain-state write failed, or the chain is invalid
modeSince v0.13.0. The governance mode (ADMINA_GOVERNANCE_MODE)
surfacesSince v0.13.0. The enabled surfaces
ruleset_sha256Since v0.13.0. The active firewall ruleset β€” the value of X-Admina-Ruleset (see /v1/admina/ruleset)
forensic_writableSince v0.13.0. Filesystem backend: a probe file is created, written, fsynced and removed. S3: the result of the last record write, null before the first. In-memory: null. A configured backend that could not be opened: false. Checked at most once every 10 s; a check that takes longer than 1 s reports false
forensic_chainSince v0.13.0. ok, rebuilt (the chain state was rebuilt from verified records at startup; stays until acknowledged with admina forensic acknowledge-rebuild) or invalid (nothing is recorded until an operator acts); null without a stored chain, as with the in-memory store. See Domains

engine is a nested object, not a string. selection echoes ADMINA_ENGINE (default auto), active is the engine the selection resolves to and pii_active is resolved separately β€” under auto the PII path stays on Python for full recall even when Rust is available. Since v0.13.0 firewall, loop_breaker and pii name the engines of the components the proxy actually built: loop_breaker is null when no enabled surface needs one (only mcp and integration do), and pii can also be presidio or a plugin engine's name. They can differ from active β€” an admina.yaml that sets Python-only firewall keys under auto gives firewall: "python" with active: "rust".

POST /mcp auth

Main governance proxy endpoint. Accepts any MCP JSON-RPC 2.0 message, applies all 4 governance domains bidirectionally, and forwards to the upstream MCP server. The response includes governance metadata as HTTP headers.

The pipeline scans and redacts every string of the request down to 32 levels of nesting (6 before v0.13.0, when deeper text went through unscanned). With the firewall or PII redaction on, a request holding text deeper than that is blocked in enforce mode β€” a would-be block in observe / dry-run β€” with checks.scan_depth = {"action": "BLOCK", "reason": "depth_limit_exceeded"} in its forensic record.

A pluggable governance guard that raises ValueError, RuntimeError, OSError or TypeError is skipped by default β€” recorded as an ERROR entry in the request-side checks (its error is the exception's class name), logged only on the response side. With ADMINA_GUARD_FAIL_MODE=closed the exception becomes a BLOCK instead, on both the request and the response side of this endpoint. See Configuration.

curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $ADMINA_API_KEY" \
  -H "X-Session-Id: my-session" \
  -H "X-Agent-Id: my-agent" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "read_file",
      "arguments": { "path": "/etc/hostname" }
    }
  }'

Request headers:

X-Session-IdSession identifier for rate limiting and loop detection, default default; truncated to 128 characters
X-Agent-IdAgent identifier recorded in the audit trail and forensic log, default unknown; truncated to 128 characters. Since v0.12.0 it is also the identity the coordination detector counts β€” it is not authenticated

There is no header to override the upstream. In single-upstream mode everything goes to UPSTREAM_MCP_URL. With multi-upstream routing configured, the target is selected either by path β€” POST /mcp/route/{server} β€” or, when no route prefix is given, by resolving the called tool name (params.name) against the routing table.

Response headers added by Admina:

X-Admina-Event-IdUnique id for this governance event (correlates with forensic log / OTEL span)
X-Admina-Governance-ActionAlways ALLOW β€” these headers are only emitted on the forwarded (success) path
X-Admina-Latency-UsGovernance overhead in microseconds
X-Admina-Forensic-HashTruncated (16-char) forensic record hash. Absent when the record was not written (since v0.13.0 a failed write is no longer reported as a hash)

Status codes:

403Governance block β€” JSON-RPC -32600, reason: "injection_detected" (see below). Also when a governance guard blocks the upstream's response
429Circuit break β€” JSON-RPC -32000, reason: "reasoning_loop"; or the Redis-backed rate limit (Rate limit exceeded, per session and per IP)
413Request content longer than MAX_REQUEST_TOKENS characters (JSON-RPC -32000, Request too large); a body over ADMINA_MAX_REQUEST_BYTES gets 413 earlier, as {"detail": ...}
400Body that is not JSON: {"detail": "Invalid JSON body"}, not a JSON-RPC error
500Since v0.13.0. The governance pipeline raised, or the upstream exchange failed in any way other than a connection error β€” including a connect or read timeout of the fixed 30 s upstream client and an upstream body that is not JSON β€” or the response side raised. JSON-RPC -32603, Internal proxy error, with event_id
502Upstream MCP server unreachable β€” a connection error only (httpx.ConnectError), JSON-RPC -32603, Upstream MCP server unreachable
503Since v0.13.0, with ADMINA_FORENSIC_FAIL_MODE=closed only: the request's forensic record could not be written, so it is not forwarded β€” JSON-RPC -32603

Denied requests carry no X-Admina-* headers at all β€” they are signalled by status code and JSON-RPC error body instead, and the governance, 500, 502 and 503 payloads include event_id (the rate-limit 429s, the MAX_REQUEST_TOKENS 413 and the 400 do not), so correlation with the forensic log still works. The block reason is fixed, not derived from the cause (still so in v0.13.0): a guard block, a response-guard block, a scan_depth block and β€” since v0.12.0 β€” an egress refusal (destination off the allowlist, undeterminable, or quarantined) also report "injection_detected". The actual cause is in the forensic record's checks β€” for egress, checks.egress.reason and checks.egress.blocked. No proxied request ever reports REDACT here: PII redaction rewrites the body in place and the request is still forwarded as an ALLOW. (REDACT is a value of the /api/v1/validate REST contract and of the action label of admina_requests_total.)

Since v0.13.0 a request is counted, emitted as a governance.decision event and stored in ClickHouse once it has been answered, with the action of its response: a response blocked by a governance guard makes the request a BLOCK (domain response_guard). Requests refused before the pipeline runs (rate limit, MAX_REQUEST_TOKENS) are counted as BLOCK; a body that is not JSON is not counted.

Governance stats

GET /api/stats auth

Aggregate governance statistics, nested one block per subsystem: the proxy's own request counters, engine diagnostics, and the per-domain counters of the firewall, loop breaker, PII redactor, forensic black box and compliance engine. Part of the dashboard surface; the dashboard session cookie is accepted here.

curl http://localhost:8080/api/stats \
  -H "X-API-Key: $ADMINA_API_KEY"
{
  "proxy": {
    "requests_total": 10482,
    "requests_blocked": 146,
    "requests_allowed": 10336,
    "requests_redacted": 35,
    "coordination_confirmed": 0,
    "coordination_suspected": 2,
    "coordination_degraded": 0,
    "prescan_accepted": 0,
    "prescan_ruleset_mismatch": 0,
    "prescan_malformed": 0,
    "prescan_ignored": 0,
    "avg_latency_ms": 41.27,
    "started_at": "2026-10-05T09:30:00+00:00"
  },
  "engine": {
    "engine": "rust",
    "rust_available": true,
    "rust_version": "0.13.0",
    "selection": "auto",
    "active": "rust",
    "pii_active": "python",
    "firewall": "rust",
    "loop_breaker": "rust",
    "pii": "python"
  },
  "firewall": {
    "total_checked": 10482,
    "total_blocked": 142,
    "block_rate": 1.35,
    "detections_by_type": { "instruction_override": 96, "role_hijack": 46 },
    "engine": "rust"
  },
  "loop_breaker": { "active_sessions": 12, "total_blocked": 4, "engine": "rust" },
  "pii_redactor": {
    "total_redacted": 35,
    "redactions_by_type": { "EMAIL": 22, "IBAN": 13 },
    "spacy_available": true,
    "engine": "python"
  },
  "forensic_blackbox": {
    "record_count": 10482,
    "chain_head": "9f2c1a7b4e8d0c35...",
    "storage_available": true
  },
  "compliance": { "total_assessments": 3, "enforcement_deadline": "2027-12-02" },
  "routing": {}
}

Since v0.12.2 (gateway) and v0.13.0 (/api/v1/validate) the requests_* counters count every governed surface β€” /mcp, /v1/chat/completions and /api/v1/validate β€” so the dashboard score counts gateway blocks too. requests_blocked counts BLOCK and CIRCUIT_BREAK, requests_allowed counts ALLOW and REDACT; a request that failed in the proxy before its decision (ERROR) counts only in requests_total. avg_latency_ms is a running average, in milliseconds rounded to 2 decimals, of the request duration from arrival to the end of the response β€” since v0.13.0 that includes the upstream's time, so it is no longer the governance overhead alone; there is no percentile series on this endpoint (see the histograms under /metrics). The three coordination_* counters (v0.12.0) count the coordination detector's verdicts, one key per recorded status (none and declared are not counted), and only /mcp feeds them. The four prescan_* counters (v0.13.0) count gateway requests by X-Admina-Scan-Policy outcome. loop_breaker is {} when no loop breaker is built (neither mcp nor integration enabled), forensic_blackbox is {} without a forensic store (the proxy normally has one, in-memory by default, where storage_available is false), and routing is {} unless multi-upstream routing is enabled.

Validation & audit

POST /api/v1/validate auth

Inline governance check. Submit content for validation without proxying to upstream. Returns the governance decision.

Breaking in v0.11.0. The response action value formerly returned as "MODIFY" is now "REDACT". There is no compatibility shim β€” external callers that compare against "MODIFY" must be updated. The in-repo n8n node and Cheshire Cat AI plugin were updated in the same release (see Integrations); the OpenClaw skill needed no change because it never referenced the value. 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.

Request body. A JSON object. content is required and must be a non-empty string: a missing or empty content returns HTTP 400 with detail 'content' field is required, and since v0.13.0 any other type returns HTTP 400 with 'content' must be a string (an object or array used to be scanned as nested data on the Python engine and answered 500 on the Rust engine). Optional: session_id (defaults to rest-<8 hex>), agent_id (defaults to rest-api), request_id (defaults to a uuid4 hex). All other keys are ignored. A non-object JSON body is rejected by FastAPI with HTTP 422.

Response body. Exactly five keys, always present: action, risk_level, checks, redacted_content, latency_ms. action is exactly one of the three values below β€” nothing else is reachable on this endpoint.

ALLOWClean traffic
BLOCKInjection firewall fired, or the loop breaker fired β€” a loop circuit-break is reported to REST consumers as BLOCK, never as CIRCUIT_BREAK. Since v0.12.0 also an egress refusal under ADMINA_EGRESS_MODE=enforce (see below). The 32-level scan_depth block cannot trigger here: content is a single string
REDACTRequest allowed but PII was found and masked

redacted_content is a string only when action is "REDACT"; it is null for ALLOW and BLOCK. Note the casing asymmetry: the top-level risk_level is uppercase (LOW / MEDIUM / HIGH / CRITICAL), while every risk_level nested inside checks is lowercase. latency_ms is a float rounded to 2 decimals. The checks keys are conditional, not fixed: loop_breaker is always present, firewall is present only when the loop breaker did not fire, pii_redaction is present only when the request is still allowed after the firewall stage, and egress (v0.12.0) is present when egress control is enabled for the integration surface and the request is still allowed after PII redaction. On this endpoint the egress stage reads the content string, so it finds a destination only when content begins with a URL or is an IP literal β€” under enforce such a request is refused unless the host is on the allowlist. See Egress control.

curl -X POST http://localhost:8080/api/v1/validate \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $ADMINA_API_KEY" \
  -d '{"content": "Please ignore previous instructions", "session_id": "test"}'
# Response β€” injection detected
{
  "action": "BLOCK",
  "risk_level": "HIGH",
  "checks": { "loop_breaker": {...}, "firewall": { "is_injection": true, "risk_level": "high", ... } },
  "redacted_content": null,
  "latency_ms": 0.42
}
curl -X POST http://localhost:8080/api/v1/validate \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $ADMINA_API_KEY" \
  -d '{"content": "Contact me at john.doe@example.com"}'
# Response β€” PII redacted (was "MODIFY" before v0.11.0)
{
  "action": "REDACT",
  "risk_level": "LOW",
  "checks": { "loop_breaker": {...}, "firewall": {...}, "pii_redaction": { "count": 1, "entities": [...] } },
  "redacted_content": "Contact me at [EMAIL]",
  "latency_ms": 1.13
}

This endpoint honours ADMINA_GOVERNANCE_MODE: in observe or dry-run mode a blocked request comes back as action: "ALLOW", but the mode downgrade does not suppress REDACT β€” a clean request carrying PII still returns REDACT with populated redacted_content. It always runs the firewall and PII redaction, whatever INJECTION_FAST_PATH_ENABLED and PII_REDACTION_ENABLED say, and runs no pluggable governance guards. A guard error in checks is returned as "Guard error", never the exception.

Since v0.13.0: a pipeline that raises is answered HTTP 500 ({"detail": "Internal Server Error"}, logged by exception class only); with ADMINA_FORENSIC_FAIL_MODE=closed the endpoint answers 503 while the forensic store does not accept records: its last write failed, the chain is invalid, or the configured backend could not be opened. Each request is counted on /metrics (surface integration), emits one governance.decision event (live feed, OpenTelemetry, one alert per block) and, with ClickHouse configured, stores one governance_events row of type validate_request. It writes no forensic record.

POST /api/v1/audit auth

Submit a forensic log entry. Used by integrations (LangChain, CrewAI) for passive governance logging. Accepts the API key or, since v0.13.0, ADMINA_AUDIT_APPEND_KEY β€” a key that can append records here and do nothing else.

curl -X POST http://localhost:8080/api/v1/audit \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $ADMINA_API_KEY" \
  -d '{"event": {"event_type": "tool_call", "agent_id": "crewai-01"}}'
# Response
{
  "recorded": true,
  "sequence_number": 42,
  "record_hash": "28c61c2b1c389a2a...",
  "previous_hash": "1ef2510988f2ec99..."
}
eventRequired, a JSON object β€” anything else returns HTTP 400. event_id and timestamp are filled in when absent
sourceSince v0.13.0 always set by the proxy to api_v1_audit; a source sent by the caller is kept as client_source
submitted_bySince v0.13.0, stamped by the proxy: the credential the request was admitted with β€” append_key, user:<id> for an auth provider's user, api_key, or unauthenticated
event_typeSince v0.13.0 the types the proxy writes itself are refused with 400 and nothing is recorded: mcp_request, mcp_response, gateway_request, gateway_response, gateway_response_scan, policy_violation, chain_state_rebuilt (compared case-insensitively, surrounding blanks ignored)

When the record could not be written the response is "recorded": false with an error string (since v0.13.0, instead of reporting a record that was not stored), or HTTP 503 with ADMINA_FORENSIC_FAIL_MODE=closed. With the default configuration the built-in apikey auth provider admits API-key requests, so a record sent with the API key reads submitted_by: "user:api_key_user"; api_key appears only when no auth provider is loaded.

GET /api/v1/forensic/verify auth

Reads the persisted forensic records back from the configured backend β€” one at a time, in sequence order, on a worker thread β€” and checks each record's hash, its link to the previous one, contiguous sequence numbers from 1 and, when a chain-state key is set, its signature. An invalid chain is a successful report β€” HTTP 200 with "valid": false β€” not a server error; only an unexpected exception produces a 500.

curl http://localhost:8080/api/v1/forensic/verify \
  -H "X-API-Key: $ADMINA_API_KEY"

# Since v0.13.0: resume from the checkpoint of an earlier result
curl "http://localhost:8080/api/v1/forensic/verify?checkpoint=10482:<64-hex record_hash>" \
  -H "X-API-Key: $ADMINA_API_KEY"
# Response
{
  "valid": true,
  "records": 10482,
  "reason": null,
  "sequence_number": null,
  "checkpoint": { "sequence_number": 10482, "record_hash": "9f2c1a7b4e8d0c35..." },
  "signed": 10482,
  "unsigned": 0,
  "signatures_verified": true,
  "last_hash": "9f2c1a7b4e8d0c35...",
  "backend": "filesystem"
}
from_seqQuery, since v0.13.0. Verify from this sequence number on (integer ≥ 1; an invalid value is a FastAPI 422)
checkpointQuery, since v0.13.0. SEQ:HASH (the checkpoint of an earlier result; HASH is 64 lowercase hex): verify only the records after it. Malformed, or with both parameters: 400
reason, sequence_numberThe first failure and where: hash_mismatch, link_broken, missing_record, sequence_gap, state_mismatch, checkpoint_mismatch, state_missing, state_invalid, signature_invalid, unsigned, store_unavailable; null when valid
checkpointWhere to resume next time; null when invalid
signed, unsigned, signatures_verifiedRecord signature counts, and whether signatures were checked (a chain-state key is set). With a key, a record without a signature from the point signing started is unsigned; records written before the key was set count as unsigned

backend is one of memory, filesystem, s3. The in-memory store (the default) has no persisted chain: it reports "valid": true with "records": 0, whatever record_count says β€” verification needs FORENSIC_BACKEND=filesystem or s3. A configured backend that could not be opened at startup reports "valid": false, reason: "store_unavailable". A chain found invalid at startup never verifies. The record format, signatures and rebuild rules are on Domains; the CLI counterpart is admina forensic verify [--from-seq N | --checkpoint SEQ:HASH].

Dashboard

The dashboard surface. Every GET route below accepts the API key or the dashboard session cookie.

POST /api/dashboard/session auth

New in v0.12.1. Exchanges the API key β€” in X-API-Key or Authorization: Bearer; an existing session cannot mint a new one β€” for the admina_dashboard_session cookie: HttpOnly, SameSite=Strict, Path=/api/, signed with a key derived from ADMINA_API_KEY and valid for ADMINA_DASHBOARD_SESSION_TTL seconds (default 3600, 60 to 43200). It is Secure as DASHBOARD_COOKIE_SECURE says (auto since v0.13.0: over HTTPS, and over plain HTTP for any host that is not localhost, *.localhost or a loopback address). GET on the same path reports whether the caller is signed in; DELETE signs out. A live feed opened with a session is closed when the session expires. Sessions issued before v0.12.1 are not accepted.

curl -X POST http://localhost:8080/api/dashboard/session \
  -H "X-API-Key: $ADMINA_API_KEY" -c cookies.txt
# {"authenticated": true, "session": true, "expires_at": 1791266400}

Answers 404 when the dashboard is disabled (ADMINA_DASHBOARD_ENABLED=false) or the dashboard surface is off. Without ADMINA_API_KEY there is no key to exchange: the auth middleware answers 401, and the route's own 404 appears only once a request is admitted (ALLOW_UNAUTHENTICATED=true or another auth provider). Responses carry Cache-Control: no-store.

GET /api/dashboard/score auth

Admina Score β€” a live-runtime weighted composite (0–100) powering the headline metric on the bundled Alpine.js dashboard. Unlike the OISG Score (static capability assessment), the Admina Score reflects what is actually happening right now on this instance: audited traffic, compliance posture, attack rate, and hash-chain integrity (plus a fixed residency credit for any running proxy).

β†’ Full formula, component breakdown, and guidance for improving each component: Admina Score.

curl http://localhost:8080/api/dashboard/score \
  -H "X-API-Key: $ADMINA_API_KEY"
# Response
{
  "score": 87,
  "max_score": 100,
  "breakdown": {
    "data_residency": 25,
    "interactions_audited": 25,
    "eu_ai_act_coverage": 12,
    "no_recent_attacks": 15,
    "forensic_chain_valid": 10
  },
  "computed_at": "2026-05-21T09:30:00+00:00"
}
GET /api/dashboard/oisg auth

OISG Adequacy Score (Open Β· Intelligent Β· Secure Β· Governed) β€” four pillars of five criteria each (5 points per criterion), for a 0–100 total. Unlike the Admina Score (live runtime metrics), this is a static capability assessment of the instance: which governance features are wired up and available. On the dashboard it appears in a separate "Instance Configuration" panel as a 2Γ—2 quadrant map. See oisg.ai and the dedicated OISG page for the full paradigm.

curl http://localhost:8080/api/dashboard/oisg \
  -H "X-API-Key: $ADMINA_API_KEY"
# Response (truncated)
{
  "total": 85,
  "max_total": 100,
  "level": "OISG adequate",
  "pillars": {
    "open":        { "score": 20, "max_score": 25, "criteria": [...] },
    "intelligent": { "score": 20, "max_score": 25, "criteria": [...] },
    "secure":      { "score": 25, "max_score": 25, "criteria": [...] },
    "governed":    { "score": 20, "max_score": 25, "criteria": [...] }
  },
  "computed_at": "2026-05-21T09:30:00+00:00"
}

Levels: 0–24 Critical gaps Β· 25–49 Partial coverage Β· 50–79 Good coverage Β· 80–100 OISG adequate.

WS /api/dashboard/live auth

Live governance event stream via WebSocket, fed by the event bus. Used by the dashboard for real-time updates. Since v0.12.2 / v0.13.0 it carries the governance.decision events of every governed surface β€” /mcp, the gateway and /api/v1/validate β€” whose metadata holds names, counts and hashes, never request text. Accepts the API key header, the dashboard session cookie or the deprecated ?api_key= parameter; a connection from an origin outside CORS_ORIGINS is closed (code 1008).

wscat -c ws://localhost:8080/api/dashboard/live \
  -H "X-API-Key: $ADMINA_API_KEY"

Other dashboard routes (all GET, read-only):

/api/dashboard/feedRecent governance events, paginated (limit 1–1000, default 50; offset)
/api/dashboard/trendEvent counts per time bucket and action (window_hours up to 720, bucket_minutes)
/api/dashboard/suggestionsStatistical (no-LLM) policy suggestions from recent events; ?format=csv available
/api/dashboard/complianceEU AI Act gap-analysis summary, with the enforcement_deadline the dashboard countdown reads; read-only (never creates an assessment)
/api/dashboard/sovereignty, /infra, /modelsData-zone statistics, infrastructure health, model and engine status
/api/eventsRaw governance_events rows from ClickHouse (limit up to 1000); without ClickHouse, an empty list with "error": "ClickHouse not available". Not reachable with the session cookie

Without ClickHouse (since v0.12.2). feed, trend and suggestions no longer answer empty with "error": "ClickHouse not available": they read the forensic black box's recent records β€” the last 1,000 written by the running proxy, kept in memory with every backend, not read back after a restart β€” one event per governed request in the same columns as a ClickHouse row, and the answer carries "source": "forensic_recent". Only requests that write a forensic record appear there: /mcp and the gateway, not /api/v1/validate.

Compliance

Decision-support, not legal advice. Admina's compliance modules (EU AI Act, NIS2, GDPR, cross-regulation matrix) are self-assessment aids. A passing score in gap-analysis does not constitute legal compliance and does not replace conformity assessment under EU AI Act Art. 43, NIS2 designated authority audit, or GDPR DPO review. See Compliance domain for scope.

EU AI Act

POST /api/compliance/classify auth

Classify an AI system's risk level under the EU AI Act risk taxonomy. Classification is keyword-driven over description, use_case and data_types, and returns one of four categories β€” unacceptable, high, limited, minimal β€” together with the static profile of that category. Since v0.13.0 it also matches Italian, French and German phrases (Art. 5 practices, Annex III areas, Art. 50 cases), so a non-English description can now get a higher class than before; English results are unchanged. The gap analysis against Art. 9–15 is a separate endpoint, POST /api/compliance/gap-analysis (below).

curl -X POST http://localhost:8080/api/compliance/classify \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $ADMINA_API_KEY" \
  -d '{
    "description": "AI credit scoring system for consumer loans",
    "use_case": "financial risk assessment",
    "data_types": ["financial", "personal", "behavioral"]
  }'
{
  "risk_category": "high",
  "level": 3,
  "description": "High-risk AI systems requiring conformity assessment",
  "examples": [
    "credit scoring",
    "recruitment",
    "law enforcement",
    "critical infrastructure",
    "healthcare diagnostics"
  ],
  "action": "Requires conformity assessment, logging, human oversight",
  "matched_terms": [
    { "lang": "en", "risk": "high", "area": "keywords", "term": "credit scor" },
    { "lang": "en", "risk": "high", "area": "keywords", "term": "financial" }
  ],
  "matched_areas": ["keywords"]
}

level is an ordinal: 4 unacceptable, 3 high, 2 limited, 1 minimal. examples and action are the static profile of the matched category, not an analysis of the submitted system. matched_terms and matched_areas (new in v0.13.0) say which terms decided the class β€” an Italian CV-screening description, for instance, matches area: "employment". No gaps, article, justification or enforcement_deadline field is returned here. The Annex III enforcement deadline (2027-12-02, postponed from 2 Aug 2026 by the Omnibus VII agreement of 7 May 2026) is exposed by the compliance block of /api/stats.

POST /api/compliance/gap-analysis auth

Run a gap analysis against the Art. 9–15 requirements. Body: risk_category (default high) and current_compliance, a map of requirement key to a list of booleans, one per check. Returns compliance_score, total_checks, passed_checks, gaps, gap_count, status, enforcement_deadline and assessed_at. For a limited or minimal category it short-circuits to "applicable": false with a message. This is the call that feeds the eu_ai_act_coverage component of the Admina Score. POST only β€” a GET answers 405.

POST /api/compliance/report auth

Generate a structured EU AI Act report for one submitted system. The body carries the system's own facts β€” system_name, description, use_case, data_types and current_compliance β€” and the call runs classification and gap analysis in a single shot, returning the combined report. Nothing is read from stored state, but the gap analysis it runs internally is recorded β€” it becomes the latest EU AI Act assessment seen by the GET report below and by the Admina Score.

GET /api/compliance/report auth

Same path, different report: a consolidated snapshot of the running instance, with no body. It aggregates the latest EU AI Act assessment, the latest NIS2 assessment, the GDPR RoPA stats and records, the cross-regulation matrix, the proxy's runtime metrics and the forensic black box stats. Serialisation is selected with ?format=json (default), csv or markdown.

NIS2 β€” self-assessment

GET /api/compliance/nis2/areas auth

List the 10 NIS2 Art. 21(2) measure areas with 4 controls each β€” 40 checks total: policies on risk analysis and information system security; incident handling; business continuity (backup, disaster recovery, crisis management); supply chain security; security in network and IS acquisition, development, and maintenance; policies and procedures to assess effectiveness of measures; basic cyber hygiene practices and cybersecurity training; policies and procedures regarding the use of cryptography; human resources security, access control policies, asset management; multi-factor authentication and secure communications.

POST /api/compliance/nis2/assess auth

Submit your self-assessment answers and receive an area-by-area gap report.

GDPR β€” RoPA & DPIA

GET /api/compliance/gdpr/records auth

List Article 30 Records of Processing Activities (RoPA). The registry is in-memory by default; persistence is opt-in through the ADMINA_GDPR_ROPA_PATH environment variable (or an explicit storage_path= when constructing the registry in-process). Note that the commented gdpr.ropa_path key in admina.yaml.example is inert β€” the config loader has no gdpr section and the proxy builds the registry with no arguments, so only the environment variable takes effect.

POST /api/compliance/gdpr/records auth

Create a new Art. 30 record. Typed schema covering controller, purposes, categories of data, recipients, transfers, retention, and security measures.

Per-record CRUD also available at GET / PUT / DELETE /api/compliance/gdpr/records/{activity_id}.

POST /api/compliance/gdpr/dpia/template auth

Generate an Art. 35 Data Protection Impact Assessment scaffold from operator-supplied facts. Returns a Markdown template populated with the provided context, structured for legal review.

Cross-regulation matrix

GET /api/compliance/matrix auth

Hand-curated mapping of 12 operational controls across EU AI Act, NIS2, and GDPR. Surfaces obligations that satisfy multiple frameworks in a single control β€” useful to avoid duplicate audit work.

OpenAI-compatible gateway

New in v0.11.0, rebuilt for embedded deployments in v0.13.0. The proxy exposes an OpenAI-compatible HTTP surface (the gateway surface, /v1/*) on the same port as /mcp, so any OpenAI-compatible client or front-end can be pointed at Admina and have its traffic governed.

Upstream routes and keys

Without further configuration there is one route, default, to ADMINA_GATEWAY_UPSTREAM (default http://localhost:11434/v1, a local Ollama). Since v0.13.0 you can define named routes with ADMINA_GATEWAY_UPSTREAMS (name=url[,name=url…]) or gateway.upstreams in admina.yaml, and give each an upstream API key, sent as Authorization: Bearer <key> β€” so hosted APIs work, not only keyless servers. Settings, key files and precedence are on Configuration.

X-Admina-UpstreamRequest header on POST /v1/chat/completions and GET /v1/models: the route to use. Without it, gateway.default_upstream or else the first route. An unknown name gets 400 (invalid_request_error, code unknown_upstream) before any governance check or forensic record

The upstream never receives the caller's credentials: Authorization, X-API-Key, Cookie and X-Admina-Upstream are not forwarded, and of the caller's other headers only those listed in ADMINA_GATEWAY_FORWARD_HEADERS are (credentials, connection and body headers and X-Admina-* cannot be listed). Admina adds X-Admina-Event-Id and, when the route has one, its key. Without a key no Authorization header is sent.

POST /v1/chat/completions auth

Runs the governance pipeline inline before forwarding β€” injection firewall, PII redaction, egress control (since v0.12.0) and pluggable governance guards. Loop detection does not run on this surface. Since v0.13.0 the pipeline runs in worker threads, off the event loop (ADMINA_GATEWAY_PIPELINE_WORKERS, default one per CPU), so a governance guard must be thread-safe.

curl -X POST http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $ADMINA_API_KEY" \
  -d '{
    "model": "llama3.1:8b",
    "messages": [{"role": "user", "content": "Summarise this clause…"}]
  }'

Refused before governance. These are answered before any governance check, forensic record or upstream call, and are not counted on /metrics; they carry X-Admina-Ruleset and X-Admina-Version but no outcome headers. Bodies are in the OpenAI error format ({"error": {"message", "type", "param", "code"}}) unless noted.

400unknown_upstream β€” X-Admina-Upstream names no route
400Body that is not JSON, or (since v0.13.0) JSON that is not an object: {"detail": "Invalid JSON body"}
403Since v0.13.0, model_not_allowed (param: "model") β€” ADMINA_GATEWAY_MODELS_ALLOWLIST is set and the requested model is not on it, or no model is given
400Since v0.13.0, invalid_value (param names the field) β€” a field a limit applies to is not absent, null or an integer ≥ 1: n only while ADMINA_GATEWAY_MAX_N is set, max_tokens / max_completion_tokens only while ADMINA_GATEWAY_MAX_COMPLETION_TOKENS is set
413Since v0.13.0, prompt_too_long β€” the message text is longer than ADMINA_GATEWAY_MAX_PROMPT_CHARS characters (default 0, no limit). MAX_REQUEST_TOKENS applies to /mcp only

What is scanned. Since v0.13.0 the firewall scans every string of the request body, keys included: the messages (content, names, tool calls), the tool definitions (tools), response_format and any other field. Tool call arguments are scanned as the JSON they hold, each string separately. With the firewall on, a string nested more than 32 levels deep blocks the request in enforce mode, with checks.scan_depth in its record. ADMINA_GATEWAY_SCAN_ROLES (default system,user,assistant,tool; other roles are always scanned) and, when ADMINA_GATEWAY_SCAN_POLICY_ENABLED=true, the request header X-Admina-Scan-Policy narrow the messages only. PII redaction rewrites the text of each message β€” content (a string or the text of each part), reasoning and refusal text, tool call arguments β€” and keeps roles, names, tool call ids, image and audio parts as received.

The egress stage reads the text of the chat messages, history included, so it sees a destination in any message that begins with a URL and in every multimodal image_url part. Under ADMINA_EGRESS_MODE=enforce that has two practical effects: a vision request whose image host is not on the allowlist is refused β€” and an inline base64 (data:) image always is, since it has no host to allowlist β€” and once a message beginning with an unlisted URL is in the history, every later turn of that conversation is refused too. Since v0.13.0 agent_security.egress.surfaces can leave the gateway out of egress control. The coordination detector is not fed from this surface. See Egress control.

Optional request headers:

X-Session-IdSession identifier, default default; truncated to 128 characters
X-Agent-IdAgent identifier, default gateway; truncated to 128 characters
X-Admina-UpstreamRoute name (see above)
X-Admina-Scan-Policyv1; roles=user,tool; prescanned=source,document; ruleset=<sha256> β€” honoured only with ADMINA_GATEWAY_SCAN_POLICY_ENABLED=true and a ruleset the proxy accepts (its own, or one in gateway.prescan_rulesets); prescanned tags count only if listed in gateway.prescan_tags. A malformed or mismatched header is never refused β€” the request is scanned in full. Any API-key holder can send it, so enable it only where every caller is trusted to scan what it declares
traceparent, tracestateW3C trace context: a valid traceparent is recorded as trace_id, and both are forwarded only when listed in ADMINA_GATEWAY_FORWARD_HEADERS. With OpenTelemetry on, each call gets a gateway.chat.completions span, a child of the caller's
(request id)The header named by ADMINA_GATEWAY_REQUEST_ID_HEADER (e.g. X-Request-Id) is recorded as request_id; headers listed in ADMINA_GATEWAY_RECORD_HEADERS are recorded in context. Recorded values are cut to 128 characters

What is forwarded. The body as received, with messages replaced by the PII-redacted version when PII was found. Since v0.13.0, three settings (all off by default) narrow it: ADMINA_GATEWAY_FORWARD_FIELDS (the top-level fields forwarded; model, messages and stream always are), ADMINA_GATEWAY_MAX_N and ADMINA_GATEWAY_MAX_COMPLETION_TOKENS (larger values are lowered; a request with neither token field gets max_tokens set to the limit). They never change the messages or what the firewall scans.

Allowed responses. Upstream errors (4xx, 5xx) reach the client with their status, body and content type, streaming or not (since v0.13.0). A successful non-streaming response is returned as received while PII redaction is off; with PII redaction on (the default) its JSON is parsed and every string redacted as a whole value, keeping structural values (index, id, type, role, name, finish_reason) and sending logprobs / token_ids as null; a success body that is not a JSON object then gets 502 (code upstream_invalid_response). No wrapper object and no Admina fields are added to the body.

With "stream": true the reply is text/event-stream, relayed by ADMINA_GATEWAY_STREAM_MODE (or gateway.stream_mode):

passthroughDefault. While PII redaction is off, the upstream SSE bytes are forwarded unchanged, every field included, each event as soon as it is complete. With PII redaction on β€” the default β€” the stream is governed instead
governedEach upstream chunk is parsed and re-sent as one chunk with all of its fields β€” ids, choice indexes, roles, tool calls, finish reasons, the final usage chunk β€” every string of each choice redacted through a windowed buffer per choice and field, so an entity split across chunks is masked before any of it is sent. data: [DONE] is sent when the upstream sends it

Before v0.13.0 only choices[0].delta.content was re-emitted, so n > 1, tool-call deltas and usage chunks were lost and a streamed reply was always HTTP 200; neither is true any more. A completion whose PII redaction does not finish (over ADMINA_GATEWAY_PIPELINE_TIMEOUT, or raising) is not sent: a non-streaming one is answered as a block, a stream ends with one data: {"error": ...} event (code response_redaction_failed) and no data: [DONE].

Response headers (since v0.13.0):

X-Admina-RulesetThe active firewall ruleset hash, on every response of this route β€” the 401 of authentication and the 413 of the body limit included
X-Admina-VersionAdmina's version, on every response the route itself answers
X-Admina-Event-IdThe event_id of the call's forensic records β€” present once the request has passed the checks above, on allowed, blocked, upstream-error, timeout and failure responses alike
X-Admina-ActionALLOW or BLOCK. A policy block is downgraded to ALLOW in observe / dry-run, with X-Admina-Would-Action giving the enforce-mode action; pipeline failures and timeouts and unfinished response redaction are BLOCK in every mode
X-Admina-RiskRisk level, uppercase
X-Admina-CategoriesNames of the firewall categories that matched, comma-separated (empty when none) β€” never text
X-Admina-Record-HashThe full record_hash of the gateway_request record, written before forwarding; absent when the record was not written
Blocked requests return HTTP 200 by default. A governance BLOCK returns a synthetic OpenAI completion so OpenAI-compatible UIs render the refusal instead of erroring: finish_reason == "content_filter", with ADMINA_GATEWAY_BLOCK_MESSAGE as the message (default: This request was blocked by the Admina governance policy.). A blocked streaming request gets a single chat.completion.chunk carrying the message and finish_reason: "content_filter", followed by data: [DONE]. Since v0.13.0, ADMINA_GATEWAY_BLOCK_STATUS=403 answers instead with HTTP 403 and {"error": {"message", "type": "governance_blocked", "param": null, "code": "governance_blocked", "categories": [...]}}, streaming or not; and either way the block is visible in X-Admina-Action: BLOCK. On a request-side block the upstream is never contacted.
# Response on BLOCK β€” HTTP 200 (default ADMINA_GATEWAY_BLOCK_STATUS)
# X-Admina-Action: BLOCK   X-Admina-Risk: CRITICAL   X-Admina-Categories: instruction_override,prompt_extraction
{
  "id": "chatcmpl-admina-1a2b3c4d5e6f",
  "object": "chat.completion",
  "created": 1784200000,
  "model": "llama3.1:8b",
  "choices": [{
    "index": 0,
    "message": {"role": "assistant", "content": "This request was blocked by the Admina governance policy."},
    "finish_reason": "content_filter"
  }],
  "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
}

Blocked in every governance mode, recorded under checks.pipeline: a pipeline that raises (since v0.13.0; it was a 500), one that exceeds ADMINA_GATEWAY_PIPELINE_TIMEOUT seconds (default 0, no limit; the wait for a worker thread counts), and a PII redaction that masked text but returned no messages. Guard contract errors still follow ADMINA_GUARD_FAIL_MODE.

Failures answered by the gateway (OpenAI error format, never exception text; since v0.13.0):

504upstream_timeout (type upstream_error) β€” a timeout before a streamed response starts, or while a non-streaming body is downloaded (ADMINA_GATEWAY_TIMEOUT_CONNECT / _READ, default 30 s; _TOTAL, default none)
502upstream_error β€” any other transport failure (was a 502 Gateway upstream unreachable with a 30 s fixed timeout before v0.13.0); also upstream_invalid_response, above
503forensic_unavailable (type server_error) β€” with ADMINA_FORENSIC_FAIL_MODE=closed, the gateway_request record could not be written; the request is not forwarded
400invalid_request_body β€” the body holds a value JSON cannot encode for the upstream (NaN, an unpaired surrogate)
500internal_error (type server_error) β€” any other failure in the gateway
(stream)A failure during a stream ends it with one data: {"error": ...} event and no data: [DONE]

Forensic records. Since v0.13.0 each chat completion writes two records with the same event_id β€” count gateway_request records to count requests. Neither contains prompt text, response text or the model name.

gateway_requestWritten before forwarding: event_id, agent_id, session_id, request_id, trace_id, context, method: "chat.completions", upstream (route name), action, risk_level, categories, governance_latency_ms, checks, prescan (scan-policy outcome), ruleset_sha256, request_sha256 (SHA-256 of the RFC 8785 canonical form of the messages forwarded upstream) and, in observe / dry-run, would_action
gateway_responseWritten once the response has ended (sent whole, left by the client, or failed): request_id, upstream, stream, action, status_code, upstream_status_code, finish_reason, usage, duration_ms, response_sha256 (the bytes sent to the client), cancelled, error (exception class only)
gateway_response_scanOnly with ADMINA_GATEWAY_SCAN_RESPONSE=true (default false) and the firewall on (INJECTION_FAST_PATH_ENABLED): the firewall also checks the content of each choice. A flagged non-streaming completion is replaced by the block message in enforce mode; a stream is checked after it has been sent and the outcome only recorded. Linked to the request by request_event_id
GET /v1/models auth

Proxies the model list of the selected route (X-Admina-Upstream), sending the route's key. Without an allow-list the upstream body and status are returned as-is. This route is a passthrough: it is not governed, not counted and not written to the forensic log. Timeouts and transport failures get the same 504 / 502 bodies as chat completions.

ADMINA_GATEWAY_MODELS_ALLOWLIST (comma-separated, empty by default) filters the returned data[] by model id. Since v0.13.0 the same list is also enforced on POST /v1/chat/completions (403 model_not_allowed, above) β€” it is an access-control policy, not just a listing filter.

curl http://localhost:8080/v1/models \
  -H "X-API-Key: $ADMINA_API_KEY"
GET /v1/admina/ruleset auth

New in v0.13.0. The active firewall ruleset: ruleset_sha256 is the SHA-256 of the RFC 8785 canonical JSON in ruleset_document β€” ruleset_format, admina_version, engine, builtin patterns (or the admina-core version on Rust), pattern packs, custom patterns, disabled categories and patterns, allowed tags, heuristic threshold β€” the value carried by X-Admina-Ruleset and /health. Use it to pin gateway.prescan_rulesets and X-Admina-Scan-Policy. Because the hashed object includes admina_version (and admina_core_version on Rust), the hash changes with every upgrade, not only when the rules change: recompute pinned hashes after each one.

curl http://localhost:8080/v1/admina/ruleset \
  -H "X-API-Key: $ADMINA_API_KEY"
# Response (ruleset_document truncated)
{
  "ruleset_sha256": "<64 hex>",
  "ruleset_format": 1,
  "ruleset_document": "{\"admina_version\":\"0.13.0\",\"allowed_tags\":[],...}",
  "engine": "rust",
  "admina_core_version": "0.13.0",
  "admina_version": "0.13.0",
  "accepted_prescan_rulesets": ["<64 hex>"],
  "prescan_tags": [],
  "scan_roles": ["system", "user", "assistant", "tool"],
  "scan_policy_enabled": false
}

The gateway is still not metered by the proxy's per-session rate limiter (that is /mcp only). Since v0.12.2 / v0.13.0 every chat completion that has an event id is recorded once its response has ended: counted in the proxy block of /api/stats and in admina_requests_total{surface="gateway"} on /metrics, emitted as one governance.decision event (live feed, OpenTelemetry, one alert per block) and, with ClickHouse configured, stored as one governance_events row of type gateway_request with the response hash. A completion blocked after the upstream answered is a BLOCK of domain response_firewall (response scan) or response_pii (redaction did not finish). The gateway shares the firewall, PII and forensic instances, so the domain-level counters include its traffic too.

Metrics

GET /metrics public

Prometheus metrics endpoint. Scrape it from your Prometheus / Grafana stack. Public unless ADMINA_METRICS_REQUIRE_AUTH=true (since v0.13.0); always served, whatever ADMINA_ENABLED_SURFACES says.

curl http://localhost:8080/metrics | head

The exposition is generated inline (no prometheus_client dependency). Label values come from fixed sets only, never from a request. Families:

admina_requests_total{surface,action}Since v0.13.0 labelled: surface = gateway, mcp, integration (/api/v1/validate); action = ALLOW, BLOCK, REDACT, CIRCUIT_BREAK, ERROR. Every enabled surface has all five samples from startup. sum(admina_requests_total) is the old unlabelled counter plus /api/v1/validate (and the gateway, if upgrading from v0.12.1 or earlier β€” it has been counted since v0.12.2), minus /mcp bodies that are not JSON
admina_request_duration_seconds{surface}Since v0.13.0. Histogram: arrival of a governed request to the end of its response, upstream included
admina_governance_duration_seconds{surface}Since v0.13.0. Histogram: time the governance pipeline took to decide
admina_requests_blocked_total, _allowed_total, _redacted_totalUnlabelled totals over every governed surface (same counters as the proxy block of /api/stats)
admina_avg_latency_msGauge: mean request duration in ms (since v0.13.0 arrival to end of response; its # HELP still says pipeline latency)
admina_prescan_*_totalSince v0.13.0. Gateway requests by X-Admina-Scan-Policy outcome: admina_prescan_accepted_total, _ruleset_mismatch_total, _malformed_total, _ignored_total
admina_coordination_verdicts_total{status}Since v0.12.0. One sample per status: confirmed, suspected, degraded
admina_firewall_detections_total{category}Firewall detections per category (samples appear with the first detection)
admina_firewall_total_checked, _total_blockedFirewall inputs scanned / blocked, all surfaces
admina_loop_breaker_total_blocked, _active_sessionsLoop breaker activations; tracked sessions (gauge). 0 when no loop breaker is built
admina_pii_total_redactedPII entities redacted
admina_forensic_record_countGauge, forensic chain length (present whenever the proxy has a forensic store β€” always, in-memory by default)
admina_event_loop_lag_secondsSince v0.13.0. Histogram of how late the event loop wakes a task that sleeps 0.1 s at a time
admina_engine_infoGauge, value 1. Since v0.13.0 labels engine and firewall (both the firewall's engine β€” engine used to read rust whenever admina-core was installed), loop_breaker (none when not built), pii, pii_redaction (on/off), rust_available (yes/no), rust_version, selection, version
# Since v0.13.0 β€” one sample per surface and action
# HELP admina_requests_total Governed requests per surface and action
# TYPE admina_requests_total counter
admina_requests_total{surface="gateway",action="ALLOW"} 9120
admina_requests_total{surface="gateway",action="BLOCK"} 41
admina_requests_total{surface="gateway",action="REDACT"} 35
...
admina_requests_total{surface="mcp",action="BLOCK"} 105
...
admina_engine_info{engine="rust",firewall="rust",loop_breaker="rust",pii="python",pii_redaction="on",rust_available="yes",rust_version="0.13.0",selection="auto",version="0.13.0"} 1

Upgrading from v0.12: dashboards and alerts that select on the unlabelled admina_requests_total or on the old admina_engine_info labels need updating β€” wrap the counter in sum() to keep the old meaning (plus /api/v1/validate traffic, and gateway traffic if you come from v0.12.1 or earlier). Fixed in v0.12.0: # HELP and # TYPE are emitted once per metric family; earlier releases repeated them per sample, so a proxy with detections in two or more firewall categories served an exposition Prometheus rejects, failing the whole scrape until restart.

OpenAPI / Swagger

The full interactive API documentation is available at http://localhost:8080/docs when the proxy is running. It includes request/response schemas, authentication, and a live "Try it out" interface. ADMINA_API_DOCS_ENABLED=false turns /docs, /redoc and /openapi.json off (404); ADMINA_API_DOCS_REQUIRE_AUTH=true puts them behind the API key. The dashboard session routes are not in the schema.

# Open in browser
open http://localhost:8080/docs

# Download OpenAPI spec
curl http://localhost:8080/openapi.json