NAME
Langertha::Knarr::Admin - Admin routes that deliver, rotate and drop a routing setup layered over Knarr's own, in memory only
VERSION
version 1.103
SYNOPSIS
use Langertha::Knarr;
use Langertha::Knarr::Admin;
use Langertha::Knarr::Stack;
my $stack = Langertha::Knarr::Stack->new( config => $config, loop => $loop );
my $knarr = Langertha::Knarr->new(
loop => $loop,
admin => Langertha::Knarr::Admin->new(
key => $ENV{KNARR_ADMIN_KEY},
stack => $stack,
remote_config => 1,
),
);
# From the central service:
# PUT /knarr/admin/config { "models": {...}, "default": {...}, ... }
# PATCH /knarr/admin/keys { "gpt-4o": "sk-...", "default": "sk-..." }
# GET /knarr/admin/status
# DELETE /knarr/admin/config
# each with Authorization: Bearer $KNARR_ADMIN_KEY
DESCRIPTION
The admin routes let a central service hand a running Knarr its routing setup -- the models, the default engine, the passthrough upstreams, the keys, the Langfuse section -- as JSON, rotate the keys, and take the setup back. What arrives is held in memory only: nothing is written to a file, a cache or a log, and a restarted Knarr has forgotten it, so the central service delivers it again. knarr start enables the routes when "admin_key" in Langertha::Knarr::Config (KNARR_ADMIN_KEY) is set; "remote_config" in Langertha::Knarr::Config lets Knarr start without any model.
Layers
What Knarr started with (the config file, or the environment's config) is the start layer and is always in effect. A delivered setup is the push layer and lies over it:
modelsare merged by name: a model of the push replaces the start model of the same name, start models without a namesake stay.default,passthroughand the scalar keys (proxy_api_key,auto_discover, the timeouts, the probe settings): the pushed value wins, else the start layer's.langfuseis merged key by key (the pushedpublic_keyandsecret_keyover a starttrace_name, say).
A PUT replaces the whole push layer, never the start layer; DELETE drops the push layer and falls back to the start layer alone. Keys of the start layer that no push can touch (listen, logging, workers, ...) stay.
Routes
Every route answers JSON with Cache-Control: no-store, and no answer ever contains a key.
GET /knarr/admin/status-
200withgeneration(how many changes since the start,0before the first),applied_at(ISO 8601, UTC) andapplied_at_epoch(when the current state took effect),layers(the layers in effect:fileorenvfor the start layer, when it holds anything, thenpush),configured,remote_config,models(the names undermodels:, both layers),default_engine(whether there is one) andpassthrough(the protocols with an upstream, without their URLs). With "token_auth" in Langertha::Knarr::Config it also hastoken_auth:{ backend, connected }(connected: the token store holds a database connection at this moment; the store connects on its first lookup). Never the DSN, user, password, relation or channel, and nothing of a token. PUT /knarr/admin/config-
Sets the push layer to the JSON object in the body: the keys of "runtime_keys" in Langertha::Knarr::Config (
models,default,passthrough,auto_discover,proxy_api_key, the upstream timeouts, the capability probe settings andlangfuse), structured as in the YAML config. A key it leaves out is not set by the push (the start layer's value applies). The process settings (listen,workers,logging,admin_key,remote_config,public_url,a2a,token_auth,routes, ...) stay as they were at the start; a body that names one is refused. Undertoken_auththe admin route cannot open a hole in the token check: the start layer'stoken_authis part of every layered setup, so a push ofmodels,default,passthrough,auto_discoverorproxy_api_keyfails the config validation (400, nothing changed), and a push of the other settings (langfuse, the timeouts) keeps the same authorizer and database connection. Keys arrive literally asapi_key: anapi_key_envin a model or the default is refused, because the start environment's secrets must not follow a pushedurl. The layered setup is checked like a config file ("validate" in Langertha::Knarr::Config) and every model's engine, and the default engine, is built before anything changes; any problem answers400and the running setup stays exactly as it was. A pushedlangfusesection with keys switches tracing on (or to the pushed Langfuse); itsurlis accepted only together with its ownpublic_keyandsecret_key(the file's Langfuse keys never go to a pushed address); its keys never appear in an answer or the log. On success:200with the status above, under the next generation. PATCH /knarr/admin/keys-
Swaps API keys only: a JSON object of name to key, where a name is a model of either layer or
defaultfor the default engine (refused as ambiguous when a model is calleddefaulttoo). The key becomes that entry'sapi_keyin the push layer (a start model is copied there with the new key; the start layer itself is never changed) and itsapi_key_envis dropped. All names must be known and every key a non-empty string, or nothing changes (400).409when there is no setup yet. Applied like aPUTof the patched push layer. DELETE /knarr/admin/config-
Drops the push layer: back to the start layer alone, or, when it holds no routing setup (
remote_configwith a start of no models), to no setup, where every other request answers503.
Applying a setup
A change builds a new Langertha::Knarr::Stack -- a new Langertha::Knarr::Router, so the engine cache and the discovered models start empty -- and hands it to the Knarr in one step, under the next generation. A request that is already running, a stream in particular, goes on with the engine it started with and is not cut off. The capability probe ("probe_capabilities" in Langertha::Knarr::Config) runs again for the new router on the native server.
Keys
The admin key is the only key the admin routes accept, as Authorization: Bearer key, compared in constant time. The proxy key ("auth_token" in Langertha::Knarr) never opens them, and a request that presents the admin key to any other route is refused with 401, so the admin key never reaches an upstream. A pushed proxy_api_key equal to the admin key is refused. The admin key is held only as its SHA-256 digest.
Error messages name the problem without echoing what was sent: a body that is not JSON gets no excerpt, unknown names are counted, not listed, and every string value of the body (four characters or more) is replaced by [redacted] in a message from validation.
Wrong keys
A request without the admin key gets 401. After "free_failures" such requests in a row from one peer address, every further one locks that address out for 1, 2, 4 ... up to "max_lockout" seconds: until then its requests answer 429 with Retry-After, without even checking the key. A request with the key ends the count. Failures more than an hour apart do not add up.
Limits
The body of PUT and PATCH may be at most "max_body" bytes, else 413; under PSGI the body is not even read then, nor before the key was checked. The routes are not published anywhere: not in the provider manifest, not as a protocol route.
Each process holds its own setup, so the admin routes need a single process: Langertha::Knarr refuses "workers" in Langertha::Knarr above 1 with an admin, knarr start sets it to 1 with a warning, and under a PSGI server that sets psgi.multiprocess the routes answer 501.
key
Required, at construction only. The admin key, at least "admin_key_min_length" in Langertha::Knarr::Config characters. Only its digest is kept.
stack
Required. The Langertha::Knarr::Stack Knarr starts with; afterwards the one currently installed.
remote_config
Boolean, default 0. A start layer without any routing setup (no models, no default engine, no passthrough) is allowed: "configured" is false until the first push, and again after a DELETE.
configured
False while there is no setup to serve from ("remote_config" with an empty start layer, before the first push, or after a DELETE). Langertha::Knarr answers every route but the admin routes with 503 then.
generation
Starts at 0, one more for every change.
max_body
Largest admin request body in bytes. Default 1048576 (1 MiB).
free_failures
Wrong-key requests in a row from one address before the lockouts start. Default 2.
max_lockout
Longest lockout in seconds. Default 300.
routes
my @routes = Langertha::Knarr::Admin->routes;
The admin routes as { method, path, action } hashes.
is_key
$admin->is_key($candidate);
True when $candidate is the admin key, compared in constant time.
status
The status as a HashRef (see GET /knarr/admin/status).
handle
my ( $status, $content_type, $body, $headers ) = $admin->handle(
knarr => $knarr,
action => 'admin_put_config',
request => $req, # ->header($name)
content_length => $length,
body => sub { $bytes },
peer => $address,
multiprocess => 0,
);
Answers one admin request; $headers are further [ name, value ] pairs. Used by Langertha::Knarr and Langertha::Knarr::PSGI.
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/langertha-knarr/issues.
IRC
Join #langertha on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.