← All engines · AI
Per-role provider resolver
beta
v0.9.0 · last updated 2026-06-06 · source: cybercli.runtime._resolve_per_role_providers
§1
What it does
Reads `[digest.providers.<role>]` TOML overrides at runtime boot and builds the per-role provider map the DigestPipeline routes against — so a Pro buyer can wire Warden to local Ollama and Knight to Anthropic in one config file.
§2
Why you care
The resolver is the engine that turns 'I want different models for different Guild roles' from a hand-wavy idea into a runtime-wired routing table. Without it, every role would share the same provider; with it, the DigestPipeline can hand a Warden event to llama3.1:8b and a Knight verification to claude-sonnet without either prompt knowing the other exists.
Per-role routing is a real-money decision on Pro installs. Warden runs against every event — that's a high-volume, latency-sensitive triage call where a cheap fast model is the right answer. Knight verifies suspect findings — low-volume, accuracy-sensitive, where a frontier model's reasoning earns its keep. Without this engine, you'd pay frontier-tier rates for every Warden triage; with it, you pay them only when Knight is asked to think.
The resolver is also where the override layer is honest about its limits. A role with no override falls through to the default provider — that's the contract. Misconfigured overrides (a typo in the role name, an api endpoint your license tier doesn't allow) raise a typed error at boot and the runtime refuses to start with the bad config in scope, rather than silently degrading.
§3
How it works
On runtime boot, `_resolve_per_role_providers(config, vault, license_service)` walks `config.toml`'s `[digest.providers.<role>]` blocks. Valid role names are the keys in the GuildRegistry's runtime-routed slice (`warden`, `knight`, `marshal`, `herald`, `scribe`, `paladin` in v1). Any other role name raises `InvalidPerRoleProviderConfig`.
For each valid role block, the resolver reads the same fields as the global provider config — `lane`, `model`, `endpoint`, `temperature`, `max_tokens`, and (for api lane) `api_key_vault_ref`. It constructs a Provider instance with the per-role values, threading through the same credential-vault lookup the global provider uses.
The resolver returns a `Mapping[GuildRole, Provider]` that the DigestPipeline stores as `self.role_providers`. On each event the pipeline gates first picks a role via the GuildRegistry, then looks the role up in `self.role_providers`, then falls back to `self.provider` if the role has no override. The fallback is deliberate — overrides are additive, not required.
License-tier validation runs in line with construction. A Free-tier install that tries to wire Warden to api lane raises `TierMismatchError` at boot; the operator sees a clear message before the runtime starts serving traffic. Tier rules: Free can use local + byo_server; Starter + Pro can use api in addition. This guards both the operator's wallet and the product's pricing integrity.
§4
Configure & observe
CLI
$ cybercli doctor Reports the resolved per-role provider map under the 'digest provider wiring' check — operator sees the role-to-provider mapping the runtime built.
Dashboard surfaces
The configuration sheet for the per-role override map. Pick a role, pick a lane + model + endpoint, save. Writes the [digest.providers.<role>] block to config.toml and prompts a runtime restart.
Audit row shape
ai_role_override_set parameters.role + parameters.previous_provider + parameters.new_provider + parameters.actor on every set. The audit row carries the lane + the model (not the api key, which lives only in the vault) so a forensic replay can show 'we moved Warden to Anthropic claude-sonnet on day 14, here's the timestamp + the operator.'
§5
Status
Beta — the resolver is stable in mechanism (TOML in, provider map out, license validation, typed errors) but the upstream consumer (the multi-role orchestrator) only fires Warden in v1. The override map for Knight + Marshal + Paladin is correctly built and exposed to the dashboard, but no event hits those roles' providers until v1.1's orchestrator ships.
Shipped 2026-06-07 (API half): DigestPipeline.replace_role_providers(new_map) is a thread-safe atomic swap of the per-role provider map. Locked by an RLock so a concurrent process() sees either pre or post state, never half-swapped. The remaining piece is a config.toml mtime watcher that calls _resolve_per_role_providers + hands the result to replace_role_providers — v1.x backlog. Until then the dashboard Save flow still prompts for a restart.
Known limit: there is no per-tenant per-role override. A multi-tenant install (v1.5 + Sovereign federation) inherits the global per-role map for every tenant; per-tenant override lands when federation does.
How this engine talks to the rest