---
title: "ADR-070: User-less configuration resolution by identifier"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-070@0.35"
source: "Adr/Adr070UserlessConfigurationResolutionByIdentifier.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# 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
`LlmConfiguration` record and dispatch through the
`*ForConfiguration()` entry points. The documented lookup path,
`LlmConfigurationServiceInterface::getConfiguration()`, 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
`LlmConfigurationRepository::findOneByIdentifier()` directly (the
pattern the Integration Guide documented in Step 5), which silently
skipped two guards:

-   the `isActive` flag — a deactivated record kept serving traffic;
-   the `beGroups` access restriction — a record an admin restricted to
    specific backend groups was resolvable by anyone.

`ConfigurationResolver` 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

`ConfigurationResolver` gains
`getActiveByIdentifier(string $identifier): LlmConfiguration` as the
supported identifier lookup for user-less contexts. It throws typed
exceptions instead of returning `null`:

-   `ConfigurationNotFoundException` — no record with the identifier
    exists;
-   `ConfigurationInactiveException` (new, implements
    `NrLlmExceptionInterface`) — the record exists but is deactivated;
-   `AccessDeniedException` — the record is restricted to backend
    groups.

**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](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-052@0.35)) and resolve through the
user-aware `LlmConfigurationServiceInterface`, 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](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-066@0.35)).

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 `isActive`
    and access-restriction guards applied, instead of re-implementing them
    (or forgetting to) around `findOneByIdentifier()`.
-   Typed `inactive` vs. `not found` outcomes 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
    (`ConfigurationInactiveException`).
-   `ConfigurationResolver` stays a private, constructor-injected
    service: no `public: true` entry is added, so the audited count in
    [ADR-028](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-028@0.35) / [ADR-065](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-065@0.35) (as reduced by
    [ADR-069](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-069@0.35)) is unchanged.
