Repository navigation
v0.13.0
[0.13.0] — 2026-10-05
Minor release: an OpenAI-compatible gateway for embedded deployments
(named upstream routes with keys, streams passed through unchanged,
upstream errors propagated, request limits, the governance pipeline in
worker threads, the governance outcome on every response, request ids and
W3C trace context, request and completion records with hashes, the whole
chat completion body in the scan), secrets from files, ADMINA_CONFIG,
ADMINA_ENABLED_SURFACES, the proxy-minimal extra and an offline mode,
linear-time pattern matching, signed release images with a -slim
variant, egress checks per surface, a forensic store that writes
atomically, signs each record, verifies from a checkpoint and exports JSON
Lines, PII engines from other packages with value-only redaction and an
[OMISSIS] mask style, per-surface request metrics and governance events
without request text, stable firewall pattern ids with pattern packs and
Italian baseline patterns, a schema check of admina.yaml, the engines in
use on /health, an OISG score from external evidence, admina redteam on
external corpora, a scan depth of 32 levels with a block past it on every
surface, EU AI Act classification of Italian, French and German
descriptions, a versioned ruleset document, and an upgrade guide
(docs/guides/upgrade-0.13.md). Upgrading is recommended; read the guide
first, since several defaults and failure modes change.
Security
-
Firewall patterns match in linear time on long inputs. Categories, risk
levels and matching results are unchanged. -
PII redaction and the spaCy + regex PII engine match e-mail addresses in
linear time on long inputs. Detected spans are unchanged. -
Each forensic record is signed:
record_sigis the HMAC-SHA256 (64
lowercase hex characters) of the ASCII characters of itsrecord_hash,
under a key derived from the chain-state key (ADMINA_FORENSIC_STATE_KEY
or_FILE): HMAC-SHA256 ofadmina-forensic/1 record signatureunder that
key;record_sig_algishmac-sha256. A record written without a key has
record_sig_alg: "none"and norecord_sig.record_hashis the SHA-256
ofjson.dumps(record, sort_keys=True, default=str)of the record without
record_hash,record_sigandrecord_sig_alg
(forensic_integrity.HASH_EXCLUDED_FIELDS); for a record without the two
signature fields it is computed as before. Verification with the key
(the store's own, orstate_keyofverify_directory(), or the key in
the environment ofadmina forensic verify) checks every signature
(reasonsignature_invalid) and requires one from the chain state's new
signed_fromon (reasonunsigned); it reportssigned,unsignedand
signatures_verified. Records written before a key was set are reported
as unsigned.record_signing_key()andsign_record_hash()are in
admina.domains.compliance.forensic_integrity. -
ADMINA_FORENSIC_STATE_KEY_FILEinside the forensic directory is refused
(SecretFileError): the key that signs the chain state and the records is
kept outside the store. -
The forensic chain state is rebuilt only from verified records. At
startup a chain state that is missing (with records) or whose HMAC does
not verify is rebuilt only when every stored record verifies with the key
from record 1 on (sequence, hashes, links, signatures); the rebuild is
logged atCRITICALand recorded as a signed record of type
chain_state_rebuilt(EventType.CHAIN_STATE_REBUILT, withcause,
records_verified,head_hash). Without a key, or when a record does not
verify, or when the last record of a valid chain state is missing or
differs, the chain is invalid:chain_statusisinvalid, aCRITICAL
log names the reason and the record, no record is written (inclosed
mode governed requests are answered503), and verification is never
valid. A record found after the last saved chain state is counted only
when it verifies and links to it. -
Verification requires contiguous sequence numbers from 1: a record
missing before the first one found, between two records or before the
chain state's count is reported asmissing_record, and a sequence
number that does not match its file, comes twice or out of order as
sequence_gap.verify_directory()also checks the chain state's HMAC
with the key and reportsstate_missingandstate_invalid;
verify_bucket()does the same for an S3 bucket, andadmina doctor
uses it, writing nothing. -
GET /healthreportsforensic_chain:ok,rebuilt,invalid(then
statusisdegraded), ornullwithout a stored chain. -
A record signature is valid only as 64 lowercase hex characters: any
otherrecord_sigis reported assignature_invalid, with or without the
key. The HMAC sidecar of the chain state (_chain_state.json.sig, white
space around it left out) is read as 64 lowercase hex ASCII characters;
any other content, text that is not ASCII or bytes that are not UTF-8
included, is an invalid chain-state signature (state_invalid; at
startup the state is then rebuilt from verified records, or the chain is
invalid). Signatures are compared in constant time as ASCII bytes
(forensic_integrity.hex_digest_matches()andstored_hex_digest()), in
the forensic store,verify_directory(),verify_bucket()and the
built-infilesystemforensic store plugin. -
POST /api/v1/auditstamps each record:sourceis always
api_v1_audit(asourcesent by the caller is kept asclient_source)
andsubmitted_byis the credential the request was admitted with
(api_key,append_key,user:<id>for an auth provider's user, or
unauthenticated).ADMINA_AUDIT_APPEND_KEY(or_FILE) is a key
accepted by this route only, besides the API key; every other route
refuses it. Unset (the default), the route needs the API key. An
event_typeof the records the proxy writes itself (mcp_request,
mcp_response,gateway_request,gateway_response,
gateway_response_scan,policy_violation,chain_state_rebuilt;
integration.PROXY_RECORD_TYPES, compared without case and surrounding
white space) is refused with400, and nothing is recorded. -
PII redaction reads text values and keeps the structure around them.
_deep_redact(MCP tool parameters and results,GovernedAgent) passes
the values of a dict to the PII engine and keeps its keys;
redact_keys=True, andGovernedAgent(redact_keys=True), redacts the
keys too. The gateway redacts the text of each chat message
(redact_chat_paramsofadmina.domains.governance, the new
redact_paramsargument ofrun_pipeline):content, as a string or as
thetextof each part, reasoning and refusal text, and tool call
arguments; roles, names, tool call ids, image and audio parts are
forwarded as received. A request whose PII redaction masked text but
returned no list of messages is blocked in every governance mode,
answered asADMINA_GATEWAY_BLOCK_STATUSsays with
X-Admina-Action: BLOCK, recorded withchecks["pipeline"]
({"action": "ERROR", "error": "redacted_messages_missing"}) and logged
as an error. -
A mask of Admina already in the text (a placeholder: the
maskof a
category ofPII_CATEGORIES, such as[IBAN]or[LOCATION], a category
name in square brackets, such as[IP_ADDRESS], or[OMISSIS]) is not
masked again: the NER step of thespacy-regexengine, thepresidio
engine andPIIEngineBridgemask a detected span only outside the
placeholders. Other text in square brackets is masked like any other text
(masking.placeholder_pattern()). -
The
presidioengine masks overlapping detections as one span, their
union, with the category and mask of the first. -
The
governance.decisionevent of an/mcprequest carries names,
counts and hashes (admina.proxy.decisions.Decision): its metadata is
surface,event_id(of the request's forensic record),domain(the
part of the pipeline that decided),latency_us,categories(firewall
category names),pii_count,request_sha256(the SHA-256 of the
JSON-RPC request as the proxy serialises it) and, inobserveand
dry-runmode,would_action. The dashboard live feed, the OpenTelemetry
exporter (a span attributeadmina.meta.<key>per key) and the alert
channels read this metadata. -
A blocked
/mcprequest sends one alert to each alert channel, built from
itsgovernance.decisionevent:detailsis the event's metadata. -
An exception raised while a request or a response is governed on the
gateway,/mcporPOST /api/v1/validate(by a governance guard, the PII
engine, the pipeline or the upstream exchange) is logged by its class
name, and atDEBUGwith the frames of its traceback, without its message
(admina.core.exception_log). Theerrorof a guard'sERRORcheck
(checks["guard_<name>"], request or response side, in the forensic
records and the ClickHousedetails) is the exception's class name. An
/mcprequest whose governance pipeline raises is answered500
(JSON-RPC-32603,Internal proxy error), and so is one that raises
after the upstream answered; aPOST /api/v1/validaterequest whose
pipeline raises is answered500({"detail": "Internal Server Error"}). -
The firewall of the gateway scans every string of a chat completion
request, keys included: the messages (content, names, tool calls), the
tool definitions (tools),response_formatand any other field of the
body. Theargumentsof a tool call (and of a legacyfunction_call) are
scanned as the JSON they hold, each string separately, and as they are
when they are not JSON.ADMINA_GATEWAY_SCAN_ROLESand
X-Admina-Scan-Policynarrow the messages only. A request whose body has
a string nested more than 32 levels deep, or tool call arguments nested
deeper than the JSON parser reads, is blocked inenforcemode
(would_actioninobserveanddry-run), withchecks.scan_depth = {"action": "BLOCK", "reason": "depth_limit_exceeded"}in its record.
request_texts()ofadmina.domains.agent_security.scan_policycollects
the texts and reportstruncated;run_pipeline(scan_truncated=True)
blocks. -
ADMINA_GATEWAY_MODELS_ALLOWLISTapplies toPOST /v1/chat/completions
too: a request for a model outside the list, or without a model, is
answered403({"error": {"message", "type": "invalid_request_error", "param": "model", "code": "model_not_allowed"}}) before any governance
check, forensic record or upstream call. An empty list (the default) lets
every model through. -
The dashboard container of
docker-compose.yml(dashboard/) adds
ADMINA_API_KEYonly to the dashboard's read-only routes
(/api/dashboard/*, its live feed,/api/stats);/mcpand the other
/api/routes are forwarded as received, with the caller's own key. With
ADMINA_API_KEYset the container does not start without
ADMINA_DASHBOARD_PASSWORD(HTTP Basic Auth), and it refuses a key with
characters other than letters, digits and. _ ~ + / = -. Without the
key it sends no key header, and the dashboard page signs in with the key.
Compose publishes the dashboard on127.0.0.1:3000. -
PII redaction builds the masked text in one pass over the detected spans
(masking.replace_spans()), in the regex and NER steps ofPIIRedactor,
thepresidioengine and the spaCy + regex PII engine: its time grows
linearly with the length of the text, however many spans it masks. The
masked text is unchanged. -
A rebuilt forensic chain state stays visible. After the chain state
was rebuilt from the records,forensic_chainwasrebuiltuntil the
next restart, thenok: removing the last records together with the
chain state left no lasting trace. The chain state now keeps the rebuild
(rebuilt: cause, record count, time) and/healthreportsrebuilt
until it is acknowledged. -
A forensic record whose JSON repeats a key does not verify
(hash_mismatch). The hash was computed on the record as Python's parser
reads it, which keeps the last of the repeated values, while another
parser of an exported record can keep the first. -
/mcpand/api/v1/validaterefuse text they cannot scan. Their
pipeline scanned and redacted strings down to 6 levels of nesting and let
deeper text through unscanned and unredacted: an injection nested in five
objects inside a tool call'sargumentswas allowed. They now scan and
redact 32 levels deep, as the gateway does, and with the firewall or PII
redaction on, a request holding text deeper than that is blocked in
enforcemode (a would-be block inobserveanddry-run), with
checks.scan_depth = {"action": "BLOCK", "reason": "depth_limit_exceeded"}.
admina.domains.governance.SCAN_DEPTHis the limit. -
The Presidio PII engine (
ADMINA_PII_ENGINE=presidio) asks the analyzer
only for the entity types it maps to Admina categories. It ran every
Presidio recognizer and discarded the other results; the URL recognizer
took about 1.2 ms per character on text with many dots (80 seconds on
64,000 characters). Detected spans are unchanged.
Added
-
Local make targets that mirror the CI jobs:
make ci-local,make ci-linux
andmake ci-audit(seemake help).make ci-linuxruns its container on
the CPUs inCI_LINUX_CPUS(default0-3, the size of a hosted runner). -
Pattern timing probe,
admina.domains.agent_security.pattern_timing:
probe_pattern()returns the worst search time of a regular expression on
generated 64k-character inputs (trigger words followed by runs of spaces,
tabs, commas or newlines, and repeated triggers);measure_pattern()also
names the slowest input. Use it to check
agent_security.firewall.custom_patternsbefore deploying them. -
Named upstream routes for the OpenAI-compatible gateway.
ADMINA_GATEWAY_UPSTREAMS(name=url[,name=url…]) orgateway.upstreams
inadmina.yaml(<name>: {url, api_key_file}) define the routes; the
environment variable, when set, replaces the YAML routes.
gateway.default_upstreamnames the route used when a request names none
(default: the first route). A request selects a route with the
X-Admina-Upstreamheader onPOST /v1/chat/completionsand
GET /v1/models; an unknown route name gets a 400 response in the OpenAI
error format (invalid_request_error, codeunknown_upstream) before
any governance check or forensic record. Without named routes the gateway
has one route,default, toADMINA_GATEWAY_UPSTREAM. The
gateway_requestforensic record carries the route name (upstream). -
Upstream API keys for the OpenAI-compatible gateway, sent as
Authorization: Bearer <key>:ADMINA_GATEWAY_UPSTREAM_API_KEYor
ADMINA_GATEWAY_UPSTREAM_API_KEY_FILEfor every route, overridden per
route byADMINA_GATEWAY_UPSTREAM_<NAME>_API_KEY[_FILE](<NAME>: route
name in upper case) or by the route'sapi_key_file. Key files are read
once at startup, with one trailing newline removed. The proxy does not
start when a key file is missing, unreadable or empty, when a key is set
both directly and as a file, when a route is malformed or when
default_upstreamnames no route. Keys are masked in the settings
representation and are not logged. Without a key noAuthorization
header is sent. The caller'sAuthorization,X-API-Key,Cookieand
X-Admina-Upstreamheaders are not forwarded upstream. -
admina.core.secretfile:read_secret_file()andresolve_secret()
resolve a secret setting given directly or as<SETTING>_FILE. -
Request body cap on every route:
ADMINA_MAX_REQUEST_BYTES(default
10 MiB,0= no limit). A body over the cap gets 413 before it is
parsed: at once when itsContent-Lengthis over the cap, otherwise as
soon as the bytes read go over it. The 413 body is in the OpenAI error
format on/v1(invalid_request_error, coderequest_too_large) and
{"detail": ...}elsewhere. -
Upstream timeouts and connection pool of the OpenAI-compatible gateway,
which has an HTTP client of its own:ADMINA_GATEWAY_TIMEOUT_CONNECTand
ADMINA_GATEWAY_TIMEOUT_READ(default 30 seconds),
ADMINA_GATEWAY_TIMEOUT_TOTAL(the whole upstream exchange; default 0),
ADMINA_GATEWAY_MAX_CONNECTIONS(default 100) and
ADMINA_GATEWAY_MAX_KEEPALIVE_CONNECTIONS(default 20). A timeout of 0
means no limit. A timeout before the response starts gets 504 and any
other transport failure 502, with an OpenAI-style error body (type
upstream_error, codeupstream_timeoutorupstream_error) that
carries no exception text. A failure during a stream ends it with one
data: {"error": ...}event and nodata: [DONE]. -
ADMINA_GATEWAY_STREAM_MODE, orgateway.stream_modeinadmina.yaml
(the environment variable wins):passthrough(default) orgoverned. -
ADMINA_GATEWAY_MAX_PROMPT_CHARS(default0, no limit): the longest
message text ofPOST /v1/chat/completions, in characters (the text of
every message). A longer request gets 413 (invalid_request_error, code
prompt_too_long) before any governance check.MAX_REQUEST_TOKENS
applies to/mcponly. -
admina.core.jcs.canonicalize(): the RFC 8785 (JSON Canonicalization
Scheme) serialisation of a JSON value, as UTF-8 bytes. -
ruleset_sha256()(admina.domains.agent_security.ruleset): the SHA-256,
as 64 lowercase hex characters, of the RFC 8785 serialisation of
{"admina_version", "engine", "builtin", "pattern_packs", "custom_patterns", "disabled_categories", "heuristic_threshold_milli"},
an object of strings and integers only.builtinlists the active builtin
patterns ({regex, category, risk_level}, in order, without those of a
disabled category) for thepythonengine and is
{"admina_core_version": ...}for therustengine;custom_patterns
are the entries as the firewall loads them;disabled_categoriesare
sorted without duplicates;heuristic_threshold_milliis the threshold ×
1000, rounded. The module imports neither FastAPI nor the proxy.
agent_security.firewall.pattern_packs(a list of names) is read from
admina.yamland is part of the hash. -
The proxy computes
ruleset_sha256()at startup for the engine its
firewall runs on. EveryPOST /v1/chat/completionsresponse carries it in
X-Admina-Ruleset, the 401 of authentication and the 413 of the request
size limit included; an unexpected failure before the response starts gets
a 500 in the OpenAI error format (codeinternal_error) with the header.
GET /v1/admina/ruleset(API key required) returns it withengine,
admina_core_version,admina_version,accepted_prescan_rulesets,
prescan_tags,scan_rolesandscan_policy_enabled. -
Scan scope of the gateway.
ADMINA_GATEWAY_SCAN_ROLES(default
system,user,assistant,tool) sets the message roles the firewall scans;
messages with any other role are always scanned. With
ADMINA_GATEWAY_SCAN_POLICY_ENABLED=true(defaultfalse) a request can
narrow the scan withX-Admina-Scan-Policy: v1; roles=user,tool; prescanned=source,document; ruleset=<sha256>: only the listed roles, and
without the text of<tag …>…</tag>blocks of the listed tags that are
also ingateway.prescan_tagsofadmina.yaml. The policy applies only
whenrulesetis the proxy's own or one ingateway.prescan_rulesets;
otherwise, when the header is malformed, or while scan policies are off,
the request is scanned in full. Unclosed, nested or stray tags leave the
whole text to the scan. Any caller that holds the API key can send the
header, so scan policies are for deployments where every such caller is
trusted to scan what it declares (see the README). Thegateway_request
forensic record carries the outcome asprescan(accepted,status,
roles,tags,ruleset), and/metricscounts
admina_prescan_accepted_total,admina_prescan_ruleset_mismatch_total,
admina_prescan_malformed_totalandadmina_prescan_ignored_total. -
ADMINA_GATEWAY_PIPELINE_WORKERS(default0, the number of CPUs): the
worker threads that run the gateway's governance pipeline, the most
requests governed at once.ADMINA_GATEWAY_PIPELINE_TIMEOUT(default0,
no limit): seconds a request waits for its governance decision, the wait
for a thread included; past it the request is blocked in every governance
mode and recorded withchecks.pipeline(time_budget_exceeded). The
same budget bounds the PII redaction of each completion and stream line. -
admina_event_loop_lag_secondson/metrics: a histogram of how late the
event loop wakes up a task that sleeps 0.1 s at a time. -
ADMINA_GATEWAY_SCAN_RESPONSE(defaultfalse): the firewall also checks
the content of each choice of a chat completion. A non-streaming completion
flagged inenforcemode, or whose check runs over the time budget, is
replaced by the block message; a streamed completion is checked after it
has been sent and the outcome is only recorded. Each check writes a
forensic record of typegateway_response_scan, linked to the request
record byrequest_event_id. -
scripts/bench_gateway.py: time to the first chunk added by the gateway
and event loop lag on a retrieval-augmented trace, per firewall engine. -
ADMINA_CONFIG: theadmina.yamlto load, for example
/etc/admina/admina.yaml. When it is set,load_config()reads exactly
that file, and so do the proxy, the firewall overrides, the egress policy
and the PII engine selection; a missing, unreadable or invalid file raises
admina.core.config.ConfigFileErrorand the proxy does not start. Unset
or empty, the search in the current directory and in the package
directory is unchanged. Explicityaml_pathandsearch_pathsarguments
still take precedence. -
ADMINA_API_KEY_FILEandADMINA_FORENSIC_STATE_KEY_FILE: files holding
the API key and the forensic chain-state key, read once at startup with
one trailing newline removed. A missing, unreadable or empty file, or a
key set both directly and as a file, stops the proxy; the error names the
setting and the path, not the key. The built-in filesystem forensic store
plugin readsADMINA_FORENSIC_STATE_KEY_FILEtoo.
admina.core.secretfile.secret_from_env()resolves such a pair of
environment variables. -
ADMINA_ENABLED_SURFACES: the surfaces the proxy serves, comma-separated
(empty = all):gateway(/v1/*),mcp(/mcp,/mcp/*),
integration(/api/v1/*),compliance(/api/compliance/*) and
dashboard(/api/dashboard/*with the live feed and the browser
sign-in,/api/stats,/api/events, the dashboard shell). The routes of
a disabled surface are not mounted and answer 404 before authentication;
/healthand/metricsare always served. An unknown surface name stops
the proxy. The startup banner lists the enabled surfaces. -
GET /healthreportsmode(governance mode),surfaces(enabled
surfaces),ruleset_sha256(the active firewall ruleset, as in
X-Admina-Ruleset) andforensic_writable(filesystem backend: a probe
file created, written, fsynced and removed inFORENSIC_BASE_DIR;s3:
the result of the last record write,nullbefore the first;memory:
null). The write check runs at most once every 10 s, on a thread of its
own; concurrent calls share it, and a check that takes longer than 1 s
reportsfalse. The other fields,engineincluded, are unchanged.
Example (ADMINA_ENABLED_SURFACES=gateway, filesystem backend, Rust
engine):{ "status": "healthy", "service": "admina-proxy", "version": "0.13.0", "mode": "enforce", "surfaces": ["gateway"], "ruleset_sha256": "<64 hex>", "forensic_writable": true, "engine": { "engine": "rust", "rust_available": true, "rust_version": "0.13.0", "selection": "auto", "active": "rust", "pii_active": "python" }, "timestamp": "2026-09-27T18:35:14.481520+00:00" } -
ForensicBlackBox.writable(): the write check behindforensic_writable. -
proxy-minimalextra: the proxy without Redis, ClickHouse, boto3, typer
and the numpy/scikit-learn stack of the Python loop breaker. It serves
the gateway surface (ADMINA_ENABLED_SURFACES=gateway, withREDIS_URL
andCLICKHOUSE_HOSTempty); with themcporintegrationsurface
enabled and neitherproxynorrustinstalled, the proxy does not
start and says which extra to install. -
ADMINA_LOG_FORMAT=json: one JSON object per log line (timestamp,
level,logger,message, andexceptionwhen there is one),
uvicorn's own lines included. Other record attributes are not written.
text(default) keeps the current format. -
ADMINA_METRICS_REQUIRE_AUTHandADMINA_API_DOCS_REQUIRE_AUTH(default
false): put/metrics, and/docs,/redoc,/openapi.json, behind
the API key. -
DASHBOARD_COOKIE_SECURE=auto: the dashboard session cookie isSecure
over HTTPS and, over plain HTTP, whenever the dashboard is addressed by a
host other thanlocalhost, a*.localhostname or a loopback address.
trueandfalse(default) keep their meaning; the usual boolean
spellings are accepted and any other value stops the proxy. -
slimtarget of the proxy Dockerfile, published as
ghcr.io/admina-org/admina-proxy:<version>-slim: theproxyextra and
the Rust engine, without thenlpandtelemetryextras and without the
dashboard files. -
The release images are pushed with an SBOM and a max-mode provenance
attestation and signed with cosign, keyless through GitHub OIDC (the
cosign verifycommand is in.github/workflows/release-docker.yml).
They carryorg.opencontainers.image.*labels, the proxy images also
org.admina.engine=rust, and theLICENSEandNOTICEfiles in
/usr/share/licenses/admina/. -
Governance outcome headers on the responses of
POST /v1/chat/completions
once the request has its event id, streaming or not, upstream errors,
timeouts and failures in the gateway included:X-Admina-Event-Id,
X-Admina-Action(ALLOWorBLOCK),X-Admina-Risk,
X-Admina-Categories(the names of the firewall categories that matched,
comma-separated; never text) andX-Admina-Record-Hash(therecord_hash
of the request record, written before the request is forwarded);
X-Admina-Would-Actioninobserveanddry-runmode. Every response of
the route carriesX-Admina-Version. A request body with a value JSON
cannot encode for the upstream request (NaN, an unpaired surrogate) is
answered400with"code": "invalid_request_body"; any other failure in
the gateway500with"type": "server_error". -
ADMINA_GATEWAY_BLOCK_STATUS:200(default: the block message as a
completion) or403({"error": {"message", "type": "governance_blocked", "param", "code": "governance_blocked", "categories"}}, streaming or not). -
ADMINA_GATEWAY_REQUEST_ID_HEADER,ADMINA_GATEWAY_RECORD_HEADERSand
ADMINA_GATEWAY_FORWARD_HEADERS: the header recorded asrequest_id, the
headers recorded incontextand the headers forwarded upstream (all empty
by default). Credentials cannot be listed; nor, for forwarding, connection
and body headers orX-Admina-*. Forwarded values outside ASCII are sent
as the bytes received. -
W3C trace context on the gateway: a valid
traceparentis recorded as
trace_idand, when listed, forwarded withtracestate. With
OpenTelemetry on, each chat completion has agateway.chat.completions
span, a child of the caller's span. -
gateway_requestrecord fields:request_id,trace_id,context,
request_sha256(the SHA-256 of the RFC 8785 canonical form of the
messagesforwarded upstream; test vectors in
tests/fixtures/jcs_vectors.json),ruleset_sha256,categories, and
would_actioninobserveanddry-runmode. -
A
gateway_responseforensic record (EventType.GATEWAY_RESPONSE) for
each gateway chat completion, with theevent_idof its request, written
once the response has ended:response_sha256(the bytes sent to the
client, counted as a stream goes out),finish_reason,usage,
duration_ms,status_code,upstream_status_code,cancelledand
error(the exception class only). -
admina.core.trace_context(W3Ctraceparentandtracestateparsing)
andOTELGovernanceExporter.start_span(). -
agent_security.egress.surfacesinadmina.yaml: the surfaces the egress
stage runs on, amonggateway(the text of the chat messages of
POST /v1/chat/completions),mcp(the arguments of/mcptool calls),
integration(/api/v1/validate) andsdk(GovernedModel.ask()and
stream()). Unset: every surface; an empty list: none. Names are
case-insensitive; an unknown name, or a value that is not a list, stops
the proxy at startup and raisesValueErrorin the SDK. Withgateway
left out, the gateway does not evaluate the text of chat messages for
destinations and its records have nochecks.egress.
admina.domains.agent_security.egressaddsEGRESS_SURFACES,
parse_egress_surfaces()andegress_policy_for(). -
ADMINA_GATEWAY_FORWARD_FIELDS,ADMINA_GATEWAY_MAX_Nand
ADMINA_GATEWAY_MAX_COMPLETION_TOKENS(all off by default: the body is
forwarded as received): the top-level fields of a chat completion
forwarded upstream (model,messages,streamand the fields of a limit
that is set always are), the largestn, and the largestmax_tokensand
max_completion_tokens. Larger values are lowered to the limit, and a
request that sets neither token field is forwarded withmax_tokensset to
it. While a limit is set, a value of its fields other than an integer of at
least 1 (absent andnullaside) is answered400({"error": {"message", "type": "invalid_request_error", "param": <field>, "code": "invalid_value"}}) before any governance check, forensic record or
upstream call. The firewall scans the request as received; the forwarded
messagesandrequest_sha256do not change.admina.proxy.gateway_body
holdsForwardSettings. -
ForensicBlackBox(fail_mode=...):open(the default) logs a record, or
a chain state after it, that cannot be written andrecord()returns
stored: falsewith no sequence number or hash;closedraises
ForensicWriteError.accepting_records()is false after a failed write
until a write succeeds again. -
Forensic chain verification reads one record at a time, in sequence
order, in constant memory, on a worker thread.verify_chain()(and the
synchronousverify()) takefrom_seqor acheckpoint
(sequence_number, record_hash)to verify only the records after it, and
return, besidesvalid,recordsandlast_hash:reasonand
sequence_number(the first failure) andcheckpoint(where to resume).
Reason codes:hash_mismatch(a record is not a JSON object, or its
record_hashis not the hash of its content),link_broken
(previous_hashis not the hash of the record before it,GENESISfor
record 1),missing_record(there is no record with the next sequence
number: before the first one found, between two records, or before the
chain state's count),state_mismatch(the record at the chain state's
count is not its head) andcheckpoint_mismatch(the record at the
checkpoint's sequence number has anotherrecord_hash).
admina.domains.compliance.forensic_integrity
(compute_record_hash(),canonical_record(),verify_entries()) and
admina.domains.compliance.forensic_files(record keys,atomic_write())
are public. -
ADMINA_FORENSIC_FAIL_MODE:open(default) orclosed. Inclosed
mode a request whose forensic record cannot be written is answered503
and not forwarded: the gateway with{"error": {"message", "type": "server_error", "param": null, "code": "forensic_unavailable"}},/mcp
with a JSON-RPC error (-32603),POST /api/v1/auditwith503;
POST /api/v1/validateanswers503until a record is written again; and
afilesystemors3backend that cannot be opened at startup stops the
proxy (ForensicBackendError). Inopenmode the failure is logged and
the request served. -
The proxy reads
domains.compliance.forensic.backend(or its older name
storage) andbase_dirfromadmina.yamlwhenFORENSIC_BACKENDand
FORENSIC_BASE_DIRare not set; the environment's values win, and a value
set in both places with different values is logged at startup. An unknown
backend inadmina.yamlstops the proxy.admina.proxy.forensic_backend
builds the store. -
GET /api/v1/forensic/verifytakesfrom_seqorcheckpoint=SEQ:HASH
(not both; a malformed value, or aSEQof more than 19 digits, is
answered400) and verifies from there. -
admina forensic export --from-seq N --format jsonl [--dir DIR] [--out FILE|-]: the records of a filesystem store from sequence number N on, in
sequence order, one per line, each the bytes of its file followed by a
newline.admina forensic verify [--from-seq N | --checkpoint SEQ:HASH]
prints the verification result as JSON and exits with 0 (valid) or 1.
Both read--dir(default$FORENSIC_BASE_DIR) and write nothing to it;
so doesverify_directory()ofadmina.domains.compliance.forensic, and
admina doctornow uses it for the filesystem backend. -
GET /healthstatusisdegradedwhile forensic records cannot be
written (forensic_writablefalse, or the last record or chain-state
write failed);healthyotherwise. -
PII engines of other packages:
get_pii_engine(ADMINA_PII_ENGINE,
pii_engineinadmina.yaml) looks a name up among the built-in engines,
then among the entry points of theadmina.pii_enginesgroup, each naming
aBasePIIEnginesubclass or a callable that returns one (aconfig
parameter receives the engine'splugin_configblock). An unknown name
raisesValueErrorlisting the built-in and the registered engines, and
the proxy does not start.admina.engines.PIIEngineBridgeis the
synchronousPIIBridgeof aBasePIIEngine: it runsdetectand
redacton an event loop of the engine's own, from any thread, and
returnsredacted_text,entities(type, offsets, length, engine name;
never the text),categoriesandcount.BasePIIEnginegains
special_categories,sentence_categoriesandsentences(text). -
ADMINA_PII_MASK_STYLE(pii_mask_styleinadmina.yaml):typed(the
default, each span replaced by the mask of its type) oromissis(each
span replaced by[OMISSIS], in every engine, in requests, responses and
streamed responses). Inomissis, thesentence_categoriesof an engine
have their whole sentence replaced (the engine'ssentences, by default
sentence_spansofadmina.domains.data_sovereignty.masking), and
StreamRedactorreleases a stream from such an engine a whole sentence at
a time, holding at mostmax_hold_chars(default 4096). Any other value
raisesValueError. -
DataClassifierclassifies the special categories of personal data of
GDPR art. 9 and 10 (SPECIAL_CATEGORIES) and those passed as
special_categories(such as an engine's) asrestricted; category
names are compared case-insensitively. -
ADMINA_PRESIDIO_NLP_MODELS(it:blank,en:en_core_web_sm) and
presidio.nlp_modelsinadmina.yaml: the spaCy pipeline of each
language of thepresidioengine, an installed model orblank(a
tokenizer with no model and no NER). Unset,en_core_web_smand
it_core_news_smare used when installed. A configured model that is not
installed, or a malformed setting, stops the engine; models are never
downloaded.get_presidio_pii_engine()keeps one engine per mask style
and pipelines. -
ADMINA_OFFLINE(defaultfalse):truesetsHF_HUB_OFFLINE,
TRANSFORMERS_OFFLINEandHF_DATASETS_OFFLINEto1before a PII engine
is built and when the proxy starts, and the proxy starts without the
OpenTelemetry exporter (OTELGovernanceExporter(enabled=False)). Values
other than true/false (1/0,yes/no,on/off) raiseValueError. -
The IBAN category of the
spacy-regexengine covers the IBAN registry:
an IBAN is masked when it has the length of its country (Italy: 27
characters), compact or with single spaces, and a valid mod-97 checksum
(admina.domains.data_sovereignty.iban). The PHONE category also covers
Italian mobile and landline numbers, with+39,0039or without. -
The gateway and
POST /api/v1/validaterecord their governance decisions
as/mcpdoes (admina.proxy.main.record_decision): each governed
request emits onegovernance.decisionevent (live feed, OpenTelemetry,
one alert perBLOCKorCIRCUIT_BREAK) and, with ClickHouse
configured, stores onegovernance_eventsrow (gateway_request, or
validate_request, the newEventType.VALIDATE_REQUEST). A gateway
request is recorded once its response has ended: its row has
response_hash(the SHA-256 of the response sent), and a completion
answered with the block message after the upstream answered is aBLOCK
ofdomainresponse_firewallorresponse_pii. The event of a
/api/v1/validaterequest has a newevent_id, and the body's
session_idwithout CR/LF, cut to 128 characters. -
/metricsserves, for the gateway,/mcpand/api/v1/validate
(surfacesgateway,mcp,integration):
admina_request_duration_seconds{surface}, a histogram of the time from
the arrival of a request to the end of its response, and
admina_governance_duration_seconds{surface}, a histogram of the time
the governance pipeline took (admina.proxy.request_metrics). -
OTEL_ENABLED(defaulttrue): with thetelemetryextra installed and
ADMINA_OFFLINEoff, the proxy exports its spans toOTEL_ENDPOINT, as
before;falsebuilds no exporter, so nothing is exported and no
connection is made for telemetry. -
Stable firewall pattern ids. Every builtin pattern has an id
(firewall.BUILTIN_PATTERNS,firewall.BUILTIN_PATTERN_IDS):
<category>.<language>.<n>for the categories of 0.12 (enfor the
English patterns,it,fr,esanddeformultilang_evasion, for
exampleinstruction_override.en.1andmultilang_evasion.it.1) and
<category>.<n>for theit_*categories. An id never changes and is
never reused.custom_patternsentries getcustom.<n>, in their order.
Each entry of the fast path'spatternscarries theidof the pattern
that matched, so the forensicchecksdo too;InjectionFirewall.pattern_ids
lists the ids a firewall applies. -
agent_security.firewall.disabled_patterns: ids of patterns the firewall
leaves out (Python engine;disabled_categoriesis unchanged). An id that
names no pattern is logged as a warning and ignored; a value that is not
a list of strings is a configuration error. Likecustom_patterns, it
makesget_firewall()use the Python firewall when the Rust engine is
selected. -
agent_security.firewall.heuristic_thresholdsets the deep-path score
from which the Python firewall flags a text (default0.5, the value it
used before; a value that is not a finite number greater than 0 stops
get_firewall()withValueError).INJECTION_DEEP_PATH_ENABLED=false
turns the deep path off on either engine:check()then returns the
fast-path result.get_firewall(deep_path_enabled=...)overrides the
variable; the proxy passes its setting (read from the environment or
.env). -
agent_security.firewall.allowed_tags: tag names (any case) the deep
path does not count as context switches, such as the tag an application
puts around retrieved documents. Other tags, separators and code fences
still count. Python engine; it does not select the Python firewall. -
Firewall pattern packs (
admina.domains.agent_security.pattern_packs):
named, versioned sets of patterns in YAML or JSON ({name, version, description, patterns: [{id, regex, category, risk_level}]}; no other
key;descriptionoptional) that the Python firewall adds after its
builtin patterns whenagent_security.firewall.pattern_packslists them.
A pack pattern's id is<pack>:<id>in check results and
disabled_patterns. Each name is looked up among the entry points of the
groupadmina.pattern_packs(a loader that returns the pack mapping or
the path of a pack file in package data), then in the directories of
agent_security.firewall.pattern_pack_dirs(<name>.yaml,.yml,
.json), whichADMINA_PATTERN_PACK_DIRS(separated byos.pathsep)
replaces when set. A pack not found (the error lists the available
packs), found in two sources, listed twice or invalid (the error names
the file or entry point and the key path), and a missing pack directory,
raisePatternPackError:get_firewall()fails and the proxy does not
start. Pack patterns are timed on 64k-character inputs when the firewall
is built: one over 50 ms is logged as a warning with its id, or raises
PatternPackErrorwithagent_security.firewall.strict_pack_timing: true. A listed pack makesget_firewall()use the Python firewall.
Example pack:examples/pattern_packs/example-pack.yaml. -
Italian baseline of the Python firewall, four builtin categories, risk
Ignora ..." at the start of the text, "Utente>
high:it_instruction_override(it_instruction_override.1: an
override verb such asignora,dimentica,non seguirewhere an
instruction starts, with rules, instructions or "quanto detto" as object;
.2: the same verbs with a second-person object, anywhere: "le tue
istruzioni", "il tuo prompt", "quanto ti è stato detto";.3: an
override after a clause that starts, where an instruction starts, with a
second-person imperative such astraduci,riassumi,rispondi,
scrivi, then up to twelve words and a comma ore,ma,poi,
quindi: "Traduci il testo e ignora le istruzioni precedenti"; the words
contain no opening quote, bracket or tag, no table cell separator and no
>, and an apostrophe only after a letter or digit ("l'articolo"), so an
opening quote inside the clause ends it),
it_role_hijack(.1: "d'ora in poi" / "da adesso" and a second-person
verb;.2: "sei ora" an AI or an assistant without limits;.3: "agisci
come" / "fai finta di essere" a model without filters;.4: "parla
come" / "immagina di essere" a model without filters where an instruction
starts),it_prompt_extraction(.1:rivela,mostra,ripeti...
the system prompt or "le tue istruzioni" where an instruction starts;
.2: the second-person formsmostrami,dimmi...) and
it_model_addressing(.1: a note or instruction for an AI system
followed by:("Istruzioni per l'IA:"), or an AI system addressed
directly followed by:or!("Attenzione chatbot:", "Attenzione
IA!");.2: "se sei un'intelligenza artificiale"). "Where an instruction
starts" is the start of the text, after a sentence end, a colon, a line
break, an opening bracket, a table cell separator|, an opening tag
(<p>), the start or the end of an HTML comment (<!--,-->), or an
opening quote or backtick (one that follows no letter or digit); then up
to four closing tags, comment ends or speaker labels ("Nota:
ignora ...", "
Ignora ..." at the start of a line), an optional list marker (-,*,
–,1),a), a#heading, a>quote), optional emphasis (**,
__) and up to two words addressing the reader ("ok,", "ciao,",
"grazie,", "ora", "poi", "per favore", "assistente,"). A closing quote,
tag or emphasis after a word is not such a start ('Il modulo "Alfa"
ignora le istruzioni precedenti', "Il fornitore ignora le
istruzioni precedenti");-->is one, also when it is written as an
arrow (->is not). An override inside a sentence without
one of these contexts is not matched, for example "Il testo è finito e
ignora le regole ricevute fin qui", or "Analizza il testo e ignora le
istruzioni precedenti" (analizzahas the form of the third person).
Every pattern is timed with the builtin patterns
(tests/test_firewall_pattern_timing.py). -
GET /healthandGET /api/statsreport, underengine, the engines of
the components the proxy built:firewall(rustorpython),
loop_breaker(nullwhen no enabled surface needs one) andpii
(python,rust,presidioor the name of a plugin engine), next to
the fields of 0.12 (selection,active,pii_active,
rust_available,rust_version,engine), which are unchanged. With an
admina.yaml that makes the firewall Python underADMINA_ENGINE=auto,
engine.firewallispythonwhileengine.activeisrust.
engine_status()takes the built objects (firewall=,loop_breaker=,
pii_engine=); without them the three fields arenull. -
admina.yaml is checked against its schema (
schema_version: 1,
admina.core.config_schema).check_config(path=None, *, strict=False)
andconfig_path()ofadmina.core.configreturn the file checked and
the paths of its unknown keys (domains.agent_security.firewal,
alert_channels[0].uri). At startup the proxy logs them as a warning,
admina.yaml <path>: unknown keys, not read: <paths> (...). -
At startup the proxy logs, as a warning, the
ADMINA_*variables of its
environment and.envfile that nothing reads (ADMINA_* variables not read by Admina: <names> (...); values are never logged). Known
are the proxy settings, the variables of the engines, the SDK, the
builtin plugins and the other containers of the stack, and
ADMINA_GATEWAY_UPSTREAM_<NAME>_API_KEY[_FILE]. Not reported:
ADMINA_<NAME>_...for each entry point<name>ofadmina.plugins,
admina.pii_enginesandadmina.pattern_packs(upper case, other
characters than letters and digits as_), and the prefixes of
ADMINA_ENV_ALLOW_PREFIXES(comma-separated). -
ADMINA_CONFIG_STRICT(defaultfalse):truemakes unknown admina.yaml
keys (ConfigSchemaError) and unknownADMINA_*variables
(UnknownVariablesError, aValueError) stop the proxy at startup. -
admina.engines.PYTHON_ONLY_FIREWALL_KEYS(custom_patterns,
disabled_categories,disabled_patterns,pattern_packs) and
admina.engines.EngineSelectionError(aValueError). -
The loop breaker and PII bridges name their engine (
engineattribute),
as the firewall bridges do. -
compute_oisg_score_from_evidence(evidence)
(admina.domains.compliance.oisg_evidence, also exported by
admina.domains.compliance): the OISG adequacy score of the 20 criteria
ofoisg.CRITERIA(same ids and labels) from evidence that the caller
supplies,{"schema_version": 1, "criteria": {"o1": {"status", "reason", "evidence_ref"}, ...}}with an entry for every criterion (JSON Schema
admina/domains/compliance/schemas/oisg-evidence.schema.json, read by
evidence_schema(); dataclassesOISGEvidenceandCriterionEvidence).
A status issatisfied,partial,accepted_gap(a known gap,
accepted with areason, which is required and not blank) or
not_applicable. Scoring:satisfiedis worth 5 points,partial2.5,
accepted_gap0;not_applicablecriteria are left out and each
pillar is rescaled to 25 over the criteria that apply; a pillar without
any has no score (null) and the total is rescaled to 100 over the other
pillars. Scores are rounded half up to one decimal, and the level is
get_level()of the total. A missing or unknown criterion, an unknown
status or key, anaccepted_gapwithout a reason, and evidence where
every criterion isnot_applicableraiseOISGEvidenceError(a
ValueError;problemsnames each key). The result,
OISGEvidenceResult(anOISGResult), carries thestatus,reason,
evidence_refandpointsof each criterion and the number of
applicablecriteria of each pillar, and exports as JSON (to_json(),
read back byfrom_dict()) and Markdown (to_markdown(), a table per
pillar).compute_oisg_score()is unchanged. -
admina redteam: the detection-efficacy scorecard as a command of the
package, with the options ofscripts/redteam.py(--engine,--corpus,
--format,--out) and--corpora-dir DIR,--config FILE(default
$ADMINA_CONFIG),--baseline FILE,--gateand--write-baseline [FILE](defaultbaseline.jsonnext to--out). With--baselineor
--gatethe run is compared with the baseline (with--gatealone, the
packaged one) and the result is printed on standard error. Exit status: 0;
1 when--gatefinds a regression (a lower recall or a new false
positive, or a corpus that ran only on engines the baseline does not
declare); 2 when an option, a corpus, the configuration or the baseline is
not valid, or when--engine rustcannot run a selected corpus
(admina-coreis not installed, or--configsets a key that only the
Python firewall applies).scripts/redteam.pyruns this command; there
--baselinewithout a file writes the baseline, as--write-baseline
does. -
run_suite()ofadmina.redteamtakescorpora_dir,baselineand
config; without them the scorecard is unchanged.corpora_dir: a directory of external corpora,<name>.jsonlfiles
whose rows have the format of the packaged corpus of their detector
(rows withmessages: loop breaker; withexpected_types: PII;
otherwise the firewall,labelattackorbenign), each listed in the
directory'sSHA256SUMS, which is verified before the run
(load_external_corpora()). They run after the packaged corpora, under
their names (the name of a packaged corpus is refused), andcorpora=
selects them by name; an unknown name incorpora=raisesValueError.
The scorecard'sexternal_corporaholdsdirand the detector of each.baseline: a baseline file (or its mapping) compared with the run by
compare(), limited to the selected corpora and engines; a corpus that
ran without the Python engine is also a failure when the baseline
declares none of the engines it ran on. The scorecard'sgateholds
baseline,failuresandnotes.BASELINE_PATHis the packaged
baseline.engines: a selected corpus that none of the selected engines can run
raisesValueErrornaming the corpora and the reason (with["rust"]:
admina-coreis not installed, orconfigsets a key of
PYTHON_ONLY_FIREWALL_KEYS).config: an admina.yaml whoseagent_security.firewallsettings
(custom patterns, pattern packs and their directories, disabled
categories and patterns, heuristic threshold, allowed tags) build the
Python injection firewall as the proxy does; the Rust engine runs on the
injection corpora only when the file sets none of
PYTHON_ONLY_FIREWALL_KEYS. The PII and loop detectors keep their
defaults. The scorecard'sconfigholdspathandpython_only_keys.
The Markdown scorecard names the external corpora and the configuration.
InjectionAdapter(config)andall_detectors(firewall_config)take the
FirewallConfig, and the adapter builds the firewall of each engine once.
-
ruleset_document()inadmina.domains.agent_security.ruleset: the
canonical JSON text whose SHA-256 isruleset_sha256(), to compare two
rulesets member by member.GET /v1/admina/rulesetreturns it as
ruleset_document, withruleset_format. -
EU AI Act risk classification in Italian, French and German.
classify_risk()also matches the phrases of
admina.domains.compliance.ai_act_terms(Art. 5 practices, the areas of
Annex III, the cases of Art. 50) on whole words of a normalised text, so
a description of a CV-screening system in Italian ishighrather than
minimal.EUAIActCompliance(term_languages=[...], extra_terms={...})
narrows the languages and adds terms of the caller. The result adds
matched_termsandmatched_areas. The English keyword lists and their
results are unchanged; a non-English description can now get a higher
class than before. -
An upgrade guide from 0.12 to 0.13:
docs/guides/upgrade-0.13.md. -
admina forensic acknowledge-rebuildand
ForensicBlackBox.acknowledge_rebuild(): verify the whole chain with the
key and clear therebuiltstatus of a chain whose state was rebuilt. -
admina.sdk.active_ruleset_sha256(): the ruleset hash of the firewall the
SDK builds fromadmina.yaml, on the engineget_firewall()selects. For
the same file andADMINA_ENGINEit is the value the proxy reports in
X-Admina-Ruleset.
Changed
-
The gateway runs the governance pipeline (firewall, PII redaction, egress
analysis, governance guards) and the PII redaction of completions in
worker threads instead of the event loop. Governance guards run there
too: one guard instance can be called by several threads at once, each
call on the event loop of its thread, so a guard must be thread-safe and
must not keep loop-bound objects across calls (seeBaseGovernanceGuard). -
A gateway request whose governance pipeline raises is blocked in every
governance mode and recorded aschecks.pipeline({"action": "ERROR", "error": "<exception class>"}; 0.12 answered 500). Guard contract errors
still followADMINA_GUARD_FAIL_MODE. -
A completion whose PII redaction runs over the time budget or raises is
not sent: a non-streaming completion is replaced by the block message, and
a stream ends with onedata: {"error": ...}event (code
response_redaction_failed) withoutdata: [DONE]. -
GuardrailsAIGuardruns one validation at a time. -
run_pipeline()takes the texts the firewall scans (scan_texts); by
default it scans every string of the body, as before. -
A malformed entry of
agent_security.firewall.custom_patternsskips only
that entry. -
Streamed chat completions pass through unchanged. With
ADMINA_GATEWAY_STREAM_MODE=passthrough(the default) and PII redaction
off, the gateway forwards the upstream SSE bytes as they are, every field
included, each event as soon as it is complete (0.12 re-emitted
choices[0].delta.contentonly). With PII redaction on, or in
governedmode, each chunk is parsed and re-serialised. -
The governed stream path sends one chunk for each upstream chunk, with
all of its fields: ids, choice indexes, roles, tool calls, finish
reasons and the finalusagechunk. With PII redaction on, every string
of a choice is redacted, per choice and per field across chunks:
content(a string or a list of parts), reasoning text, tool and
function callargumentsand any other field. The values ofindex,
id,type,role,nameandfinish_reasonand the chunk identity
are kept;logprobsandtoken_idsare sent asnull; other strings
outside the choices and SSE comment lines are redacted as whole values;
values nested more than 16 levels deep are dropped.data: [DONE]is
sent when the upstream sends it. -
Upstream errors (4xx, 5xx) reach the client of the gateway with their
status, body and content type, streaming or not. A non-streaming body is
forwarded unchanged unless PII redaction is on; with redaction on, a
successful response that is not a JSON object gets 502 (code
upstream_invalid_response), and in a JSON response every string is
redacted as a whole value under the same rules as the governed stream
(structural values and identity kept,logprobsandtoken_idsof each
choicenull).GET /v1/modelsforwards the upstream body unchanged
when no allow-list is set. -
redisis imported only for aREDIS_URLwith a Redis scheme and
clickhouse_connectonly for a non-emptyCLICKHOUSE_HOST;boto3
stays limited toFORENSIC_BACKEND=s3. WithREDIS_URLand
CLICKHOUSE_HOSTempty there is no connection attempt. A backend that is
configured while its package is missing is logged as a warning and left
off. -
The loop breaker is built only when the
mcporintegrationsurface is
enabled, the coordination detector with its quarantine refresh loop only
withmcp, and the gateway's pipeline threads only withgateway.
Without the loop breaker/api/statsreports"loop_breaker": {}and the
startup bannerLoop Breaker: OFF. -
The container entrypoint accepts
ADMINA_API_KEYorADMINA_API_KEY_FILE
and prints only whether the key is set, not any of its characters. -
Validation errors of the proxy settings name the setting without echoing
the configured values. -
The release workflows run the CI workflow on the tagged commit and
publish only when it passes. A PEP 440 pre-release tag (for example
v1.2.0rc1) makes a GitHub pre-release, and thelatestimage tags
move only with a final release. -
The proxy and dashboard images pin their base images by digest. The
proxy image build fails when the Rust engine does not build (previously
the image fell back to the Python engines). -
uv.lockresolvesadmina-corefrom./core-rust([tool.uv.sources]),
souv sync --extra rustor--all-extrasbuilds the Rust engine of the
same checkout and needs a Rust toolchain; a sync without therustextra
does not. The published package metadata keeps the version range of the
rustextra. The CI python-tests job,make ci-pythonand
make ci-linuxtest against this engine. -
scripts/check-versions.pycompares versions in their PEP 440 spelling
(1.2.0-rc.1in the Cargo files matches1.2.0rc1) and also checks the
admina-coreentry ofuv.lock. -
ADMINA_ENGINE=rustwithoutadmina-coreinstalled is an error: the
engine factories (get_firewall(),get_loop_breaker(),
get_pii_engine(), the SDK included) andengine_status()raise
EngineSelectionError("ADMINA_ENGINE=rust, but admina-core is not
installed: install admina-framework[rust], or set ADMINA_ENGINE=python
(or auto) to run the Python engines") and the proxy does not start (it
ran the Python engines, with a warning). Migration: install the[rust]
extra, or setADMINA_ENGINE=auto(Rust when installed) orpython. -
ADMINA_ENGINE=rustwith a non-empty
agent_security.firewall.custom_patterns,disabled_categories,
disabled_patternsorpattern_packsin admina.yaml is an error:
get_firewall()raisesEngineSelectionErrornaming the keys
("ADMINA_ENGINE=rust, but admina.yaml sets
agent_security.firewall.custom_patterns, which only the Python firewall
applies: remove them, or set ADMINA_ENGINE=python (or auto) to run the
Python firewall") and the proxy does not start (it ran the Python
firewall, with a warning). Migration: remove these keys from the file
used withADMINA_ENGINE=rust, or setauto(the Python firewall runs
with them, as before) orpython.pattern_pack_dirs,
strict_pack_timing,heuristic_thresholdandallowed_tagsdo not
select an engine. Underautothe warning names the keys that are set. -
A value of the wrong type in admina.yaml is an error naming the key:
load_config()raisesConfigSchemaError(aValueError; a file named
byADMINA_CONFIGgives aConfigFileError, as for other invalid files),
for example "admina.yaml /etc/admina/admina.yaml:
domains.agent_security.loop_breaker.window_size: must be an integer", and
the proxy does not start. The engine factories that read admina.yaml
(get_firewall(),get_pii_engine(),pii_mask_style(),
get_egress_policy(), the SDK included) raise the same error, whatever
key it names (get_egress_policy()gave an empty allowlist for a value
it could not read);admina plugin listexits with it, and
admina doctorreports it under plugin discovery. The values of
gatewayandpresidioare
checked by their readers, as before; empty values (null) and the
free-form blocks (plugin_config,integrations,
agent_security.domains, the entries ofcustom_patterns) are not
checked. Migration: fix the value the error names. -
The startup banner reports the engine selection and the engines that run,
and the settings that switch the firewall and PII redaction on the
gateway and/mcp: "Engine selection: ADMINA_ENGINE=auto (admina-core
)" and "Firewall: ON (rust engine) | PII Redaction: OFF (gateway
and /mcp) | Loop Breaker: ON (rust engine)" (they were "Engine: RUST
v" and "Firewall: ON | PII Redaction: ON | Loop Breaker: ON",
whatever the engines and settings). -
admina_engine_infohas the labelsengineandfirewall(the
firewall's engine;enginewasrustwheneveradmina-corewas
installed),loop_breaker(nonewhen none is built),pii,
pii_redaction(on,off),rust_available,rust_version,
selectionandversion. -
README and MODEL_CARD describe the firewall as a heuristic signal, give
the pattern counts of each engine (44 builtin patterns on Python, 15 on
Rust), the engine selection and the official image (the Rust firewall
and loop breaker underauto, no spaCy model), and state what the
engine microbenchmark measures; the full Docker Compose stack runs 8
containers. -
admina_requests_totalhas the labelssurface(gateway,mcp,
integration) andaction(ALLOW,BLOCK,REDACT,CIRCUIT_BREAK,
ERROR), with a sample for each enabled surface and action from startup;
sum(admina_requests_total)counts what the unlabelled counter counted,
plus the gateway and/api/v1/validate, less the/mcprequests whose
body is not JSON (answered400before they are governed).
admina_requests_blocked_total,admina_requests_allowed_total,
admina_requests_redacted_total,admina_avg_latency_ms(now the mean
duration of the counted requests) and therequests_*counters of
/api/statscount every governed surface, so the dashboard score counts
gateway blocks too. -
An
/mcprequest is recorded (counted on/metrics, its
governance.decisionevent, its ClickHouse row) once it has been
answered, with the action of its response: a response that a governance
guard blocks (itsinspect_responseverdict, or its contract error with
ADMINA_GUARD_FAIL_MODE=closed) makes the request aBLOCKofdomain
response_guard, with the guard'srisk_level(HIGHfor a contract
error). It is counted inadmina_requests_total{surface="mcp", action="BLOCK"}andadmina_requests_blocked_totalonly, and sends one
alert. -
The ClickHouse
request_hashof an/mcprow is the whole SHA-256 (64
hexadecimal characters), therequest_sha256of its event. -
Each gateway chat completion writes two forensic records,
gateway_requestand thengateway_response; countgateway_request
records to count requests. -
A non-streaming completion blocked by the response scan, or whose PII
redaction did not finish, is answered asADMINA_GATEWAY_BLOCK_STATUS
says, withX-Admina-Action: BLOCK. -
A chat completion request whose JSON body is not an object is answered
400(Invalid JSON body). -
The filesystem forensic store writes each record,
_chain_state.jsonand
_chain_state.json.sigatomically and durably: to a temporary file in the
same directory (.<name>.<random>.tmp, never read as a record), fsynced,
renamed into place, then the directory fsynced; a new directory is fsynced
in its parent. Files keep the permissions a new file gets. A record never
goes into an hour directory earlier than the one of the record before it. -
A forensic record that cannot be written (filesystem or S3) is not counted:
the next record takes its sequence number, so the stored chain has no gap. -
FORENSIC_BACKEND=filesystemwithout a directory (or with one that
cannot be created), andFORENSIC_BACKEND=s3without boto3 or with S3 not
reachable, no longer fall back to the in-memory store: inopenmode the
proxy starts with a store that records nothing (UnavailableForensicStore,
forensic_writable: false) and logs an error; inclosedmode it does not
start. A directory that exists but cannot be written is logged at startup
(and stops the proxy inclosedmode). -
The S3 forensic store reads its chain state and records with the retries
of its writes (FORENSIC_S3_MAX_RETRIES,FORENSIC_S3_BASE_DELAY_S), and
an object is missing only when S3 answers that it does not exist
(NoSuchKey). Any other error still there after the retries is a read
error, as for a file of the filesystem store: a chain state that cannot
be read is not used, and a record that cannot be read at startup keeps
the backend from opening (as above).verify_bucket()raises such an
error instead of reporting the chain state missing. -
POST /api/v1/auditanswers{"recorded": false, "error": ...}when the
record could not be written;/mcpsends noX-Admina-Forensic-Hashthen. -
The
admina inittemplate leaves the forensic backend to
FORENSIC_BACKENDandFORENSIC_BASE_DIR(the lines inadmina.yamlare
comments). -
The chain state also holds
format(admina-forensic/1) andhead_key
(the key of the last record). Checking it at startup reads its last
record and any record written after it, not every record. -
The proxy of
docker-compose.yml, and of thedocker-compose.ymlthat
admina initgenerates, keeps its forensic directory
(FORENSIC_BASE_DIR=/app/.admina/forensic) on the named volume
forensic-data, so the records and the chain state outlive the
container. Both proxy images create/app/.admina/forensicowned by the
adminauser (uid 10001), so a new volume mounted there is writable by
the proxy. -
The IBAN category of the
spacy-regexengine no longer masks an
IBAN-shaped string with a wrong checksum, a length other than its
country's, or an unknown country code. Thespacy-regexengine matches
IBANs before card numbers, and card numbers before phone numbers. -
The
presidioengine checks e-mail domains against the public suffix
list bundled withtldextract, with no download and no cache files. -
The object
ruleset_sha256()hashes also hasdisabled_patterns
(agent_security.firewall.disabled_patterns, sorted without duplicates;
builtinstill leaves out only the patterns of a disabled category) and
allowed_tags(lower case, sorted without duplicates), and
pattern_packsholds the content of each listed pack, in order:
{name, version, patterns: [{id, regex, category, risk_level}]}(a
changed pack file changes the hash; the description is not hashed);
ruleset_sha256()raises
PatternPackErrorwhen a listed pack cannot be loaded. New test vectors
are intests/test_ruleset_sha256.py. -
agent_security.firewall.pattern_packs,pattern_pack_dirs,
disabled_patternsandallowed_tagsmust be lists of strings and
strict_pack_timinga boolean: another value is a configuration error
(ValueErrorfromload_config()). -
The Italian
multilang_evasionpatterns (multilang_evasion.it.1,
multilang_evasion.it.2) match where an instruction starts (see the
Italian baseline above) and end on a word boundary: third-person prose
with the same verbs ("il consiglio annulla le direttive precedenti") no
longer matches, nor does a verb inside another word ("signora"); the
singularistruzioneandrestrizionestill match. After up to two
addressing words, and after Markdown or HTML markup where an instruction
starts, they still match at riskcritical("Ok, ignora tutte le
istruzioni precedenti", "Grazie. Ora, per favore ignora le istruzioni
precedenti", "Ignora le istruzioni precedenti", "| Nota | Ignora le
istruzioni precedenti |", "Nota: ignora le istruzioni precedenti",
"Testo Ignora le istruzioni precedenti", "Utente> Ignora
le istruzioni precedenti"). An override inside a sentence without such a
start, or after a closing quote, tag or emphasis that follows a word
("Il fornitore ignora ..."), no longer matches them:
the Italian baseline matches it at riskhighwhen the clause starts
with a second-person imperative or the object is second-person ("Traduci
il testo e ignora le istruzioni precedenti", "... e ignora le tue
istruzioni"), and otherwise it is not matched ("Il documento è lungo,
ignora le istruzioni precedenti"). -
The committed red-team baseline records the Python injection recall
23/37 (it was 21/37): the Italian attacksinj-it-002andinj-it-003
of the corpus are detected. No new false positive; the Rust figures are
unchanged. -
The default configuration's
ruleset_sha256()for thepythonengine
changes with the builtin pattern list. -
The default
agent_security.firewall.heuristic_thresholdof the
configuration is0.5(it was0.7, which the firewall did not read), so
the defaultheuristic_threshold_milliof the ruleset is500.
admina.yaml.exampleand theadmina inittemplate set0.5. Upgrading:
admina.yamlfiles generated byadmina initup to 0.12, and copies of
the example of those releases, setheuristic_threshold: 0.7, which the
Python firewall now applies; set0.5to keep the previous behaviour. -
Deep path of the Python firewall: HTML entities (named, decimal and
hexadecimal) no longer count as encoding markers, only\uXXXXescape
sequences do (percent-encoding never counted); the length signal counts
texts longer than 100 000 characters (firewall.LONG_TEXT_CHARS; it
counted texts longer than 2000). -
The object
ruleset_sha256()hashes also hasruleset_format(1),
the version of its form, so that a later change of the form is explicit
(RULESET_FORMAT). A caller that pins a hash in
gateway.prescan_rulesetsorX-Admina-Scan-Policyrecomputes it with
this release. -
POST /api/v1/validateanswers400('content' must be a string)
whencontentis not a string. An object or an array was scanned as
nested data by the Python engine, and answered500with the Rust
engine. -
The red-team gate (
admina redteam --gate,admina.redteam.gate.compare)
fails when the number of benign samples of a detector (fp_samples)
differs from the baseline, with a message that asks for a new baseline:
false-positive counts are compared only over the same benign samples.
Deprecated
- The API key in the query string (
?api_key=). It is still accepted;
the first request that authenticates with it logs a warning, once per
process, without the key. A URL ends up in access logs, proxy logs and
browser history; sendX-API-KeyorAuthorization: Bearerinstead. A
later release will refuse it.
Fixed
-
The auth middleware runs the request handler once, after the first auth
provider that returns a user. An exception raised by the handler gets
the application's 500 response and is not retried with another provider. -
GovernedModel.ask()andstream()work with the SDK alone
(pip install admina-framework, without numpy and scikit-learn). They
built the Python loop breaker on every call, loop detection on or off,
and failed withModuleNotFoundError: numpy; the loop breaker is now
built only when loop detection runs. When it runs without those
packages, theImportErrornamesadmina-framework[proxy](or[rust]). -
The
role_hijackingpattern of the Rust firewall (admina-core) matches
whole words only. It matched "act as" inside longer words, so English
text such as "impact assessment" or "the AI Act asks" was reported as a
role-hijacking attempt. Attacks written with whole words ("act as",
"you are now", "pretend you are", "from now on you") are still matched. -
The dashboard feed, trend and suggestions work without ClickHouse.
/api/dashboard/feed,/api/dashboard/trendand
/api/dashboard/suggestionsused to answer empty, with
"error": "ClickHouse not available", when no ClickHouse was configured,
as in an embedded deployment. They now read the recent records of the
forensic black box: one event per governed request (/mcp, the gateway
and/api/v1/validate), in the same columns as a ClickHouse row, and the
answer carries"source": "forensic_recent".
ForensicBlackBox.recent_records()keeps the last 1,000 records written
by the running proxy, in memory, with every backend; records written
before a restart are not read back. The WebSocket live feed keeps
reading the event bus. -
The dashboard's EU AI Act countdown shows the deadline it counts down
to. The date next to the countdown was a fixed "August 2, 2026", while
the number of days came from theenforcement_deadlineof
/api/dashboard/compliance(2 December 2027 for Annex III); both now
read that field. -
The dashboard's EU AI Act help shows a command that works. The
examplecurlforPOST /api/compliance/gap-analysistargeted
http://localhost:8080; it now targets the origin that served the page,
and the help states that the route acceptsPOSTonly (aGET, such as
opening the address in the browser, answers405 Method Not Allowed).
Notes
- As of 0.12.1, Admina is developed with AI assistance (Claude). Commits
written with it carry aCo-Authored-Bytrailer that names the
assistant, and every change is reviewed and tested by the maintainers.
See "AI-Assisted Contributions" inCONTRIBUTING.md. - The timing tests of
tests/test_firewall_pattern_timing.pycarry the
benchmarkmarker and are excluded from CI (-m "not benchmark"): on
shared macOS runners their time ratios and budgets vary between runs.
They run withpytest -m benchmark tests/test_firewall_pattern_timing.py.