ADR-070: User-less configuration resolution by identifier
- Status
-
Accepted
- Date
-
2026-07-16
- Authors
-
Netresearch DTT GmbH
Context
Downstream extensions pin their LLM calls to a named
Llm record and dispatch through the
*ForConfiguration() entry points. The documented lookup path,
Llm, enforces a
backend-user access check — unusable from user-less contexts such as CLI
commands, Symfony Messenger consumers, or anonymous frontend requests.
Consumers therefore resolved records through
Llm directly (the
pattern the Integration Guide documented in Step 5), which silently
skipped two guards:
- the
isActiveflag — a deactivated record kept serving traffic; - the
beGroupsaccess restriction — a record an admin restricted to specific backend groups was resolvable by anyone.
Configuration already existed as the access-check-free
resolution collaborator for the default-configuration path, with a
deliberate refusal policy: in a context without a backend user, an
access-restricted default is not auto-applied, because there is no user
to enforce the group membership against.
Decision
Configuration gains
get as the
supported identifier lookup for user-less contexts. It throws typed
exceptions instead of returning null:
Configuration— no record with the identifier exists;Not Found Exception Configuration(new, implementsInactive Exception Nr) — the record exists but is deactivated;Llm Exception Interface Access— the record is restricted to backend groups.Denied Exception
Access-restricted records are refused in user-less contexts. This
extends the resolver's existing default-path policy to identifier
lookup: beGroups restrictions express "only these backend groups may
use this configuration", and a context without a user cannot prove
membership — resolving the record anyway would turn the restriction into
a no-op exactly where nobody is watching (unattended workers). A
consumer that needs an access-restricted configuration in a worker
context must attribute the call to a user via the options-carried
beUserUid (ADR-052) and resolve through the
user-aware Llm, or the admin removes
the group restriction from the record.
Unlike the default path, no directly assigned model is required:
criteria-mode configurations carry no model_uid and resolve their
model at call time (ADR-066).
No pinned-client facade is added. The per-capability
*ForConfiguration() methods are already the one-line pinned call;
option building (attribution, detail levels, tool defaults) is consumer
policy that a generic facade cannot guess.
Consequences
- User-less consumers get one supported lookup with the
isActiveand access-restriction guards applied, instead of re-implementing them (or forgetting to) aroundfindOneByIdentifier(). - Typed
inactivevs.not foundoutcomes let consumers degrade differently (e.g. log-and-skip vs. configuration error). - The Integration Guide's Step 5 now routes through the resolver; the
previous example also called a non-existent
findByIdentifier()(actual method:findOneByIdentifier()). - Access-restricted configurations are unavailable to user-less callers by design — an intentional behaviour change against raw repository lookup, which ignored the restriction.
- One new exception class on the public surface
(
Configuration).Inactive Exception Configurationstays a private, constructor-injected service: noResolver public: trueentry is added, so the audited count in ADR-028 / ADR-065 (as reduced by ADR-069) is unchanged.