ADR 070 · Accepted
User-less Configuration Resolution by Identifier
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) 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).
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 / ADR-065 (as reduced by ADR-069) is unchanged.