Settings & Configuration
This page is the API reference for the configuration model: the types the host
hands to the runtime, and the typed reader services use to consume those values.
The two halves pair tightly. Settings flow in from the host as RuntimeSettings,
the registry routes the relevant slice to each service, and the service reads
its slice through ConfigNode.
For the conceptual treatment of ConfigNode, see Configuration.
For the workflow of applying settings live, see Settings & Overrides.
The settings flow
Section titled “The settings flow”host app │ RuntimeSettings (plugins map + services map) ▼PluginRuntime │ registry materializes overrides, picks the winner per slot ▼ServiceRegistry.resolve<T>(serviceId) │ injectSettings(map) on the resolved instance ▼PluginService.config (ConfigNode)The host owns a RuntimeSettings (empty, hand-built, loaded from JSON, or
produced by Plugin Kit Dialog). The registry
combines it with the priority chain to produce the effective configuration map
for each resolved service. The service reads through config, a ConfigNode
over the injected map. Defaults live in the service, not in the settings model.
RuntimeSettings
Section titled “RuntimeSettings”Top-level container for plugin enablement and service overrides. Immutable, JSON-round-trippable, value-equal.
final settings = RuntimeSettings( plugins: { const PluginId('sql_language'): const PluginConfig(enabled: true), const PluginId('experimental_router'): const PluginConfig(enabled: false), }, services: { const PluginId('linter_suite').service('line_length_linter'): const ServiceSettings(config: {'max_line_length': 120}), PluginId.wildcard.service('agent_service'): const ServiceSettings( priority: 200, config: {'provider': 'openai'}, ), },);Fields and constructors
Section titled “Fields and constructors”| Field | Type | Notes |
|---|---|---|
plugins | Map<PluginId, PluginConfig> | Keyed by PluginId, not raw String. Wrap literals as const PluginId('foo'). |
services | Map<Pin, ServiceSettings> | Keyed by Pin, an extension type wrapping the canonical pluginId:serviceId (or *:serviceId) wire string. See the key formats below. |
Both key types are typed handles, not raw Strings. A Map<String, PluginConfig> or Map<String, ServiceSettings> literal will not type-check against the corresponding field.
The default constructor is const; RuntimeSettings.fromJson(...) is a
non-const factory. The default constructor’s parameters default to
const {}. RuntimeSettings() is the canonical “no overrides” starting
point; prefer it over passing two empty maps explicitly.
const emptySettings = RuntimeSettings();Query helpers
Section titled “Query helpers”bool isPluginEnabled(PluginId pluginId);bool isServiceEnabled(Pin scopedKey);Map<String, dynamic> getPluginConfig(PluginId pluginId);Map<String, dynamic> getServiceConfig(Pin scopedKey);| Method | Returns | When the entry is missing |
|---|---|---|
isPluginEnabled | bool | true (plugins default to enabled) |
isServiceEnabled | bool | true (services default to enabled) |
getPluginConfig | Map<String, dynamic> | const {} |
getServiceConfig | Map<String, dynamic> | const {} |
isPluginEnabled reads only the explicit PluginConfig.enabled value. It
does not consult feature flags or experimental defaults. For the
base runtime enablement decision (locked plugins, explicit config,
experimental fallback), use PluginRuntime.isPluginEnabled. For the
post-cascade runtime truth, use PluginRuntime.isPluginAttached. See Plugins
& Lifecycle.
copyWith
Section titled “copyWith”RuntimeSettings copyWith({ Map<PluginId, PluginConfig>? plugins, Map<Pin, ServiceSettings>? services,}) {Returns a copy with the given replacements. When a parameter is omitted, the
result holds a shallow clone of the existing map (a fresh Map instance with
the same entries), not the original reference, so mutating the copy’s maps
does not affect the original.
Service key formats
Section titled “Service key formats”RuntimeSettings.services keys are Pin values. Two forms cover every
override:
| Pin | Targets |
|---|---|
Pin('pluginId', ['service', 'segments']) | A specific plugin’s registration for that slot. |
Pin.wildcard(['service', 'segments']) | Whichever registration currently wins that slot. |
The wire format (used by Pin.wire for JSON serialization) is
pluginId:serviceId or *:serviceId. Both forms accept namespaced service
ids (agent.temperature, mcp.tool_provider); the segments list is joined
with dots into the wire serviceId.
In Dart code, prefer the typed-chain helpers when you have the underlying
PluginId / Namespace / ServiceId handles in scope:
final settingsWithPriority = RuntimeSettings( plugins: {const PluginId('formal'): const PluginConfig(enabled: false)}, services: { Pin('chat', ['agent', 'model']): const ServiceSettings( config: {'temperature': 0.7}, ), Pin.wildcard(['agent', 'tools']): const ServiceSettings(priority: 200), },);The wildcard form is winner-scoped, not layered. The runtime resolves the
current winner first, then materializes the wildcard override onto that
winner. For the full reasoning and how resolveAfter interacts with wildcards,
see Settings & Overrides.
Parsing wire keys
Section titled “Parsing wire keys”When you receive an override key as a wire string (e.g., from JSON your
own code is parsing), build a Pin via Pin.fromWire:
/// Demonstrates Pin.fromWire parsing a wire-format key.void demonstratePinFromWire() { const pin = Pin.fromWire('main_agent:agent.temperature'); // pin.pluginId == PluginId('main_agent') // pin.serviceId == ServiceId('agent.temperature') // pin.wire == 'main_agent:agent.temperature' print('${pin.pluginId} ${pin.serviceId} ${pin.wire}');}Pin.fromWire is a const constructor that accepts any string and stores
it verbatim. Validation happens lazily on access: reading pin.pluginId
or pin.serviceId throws FormatException if the wire string has no :
separator (or its plugin half is empty). The error message names the
offending key, which makes JSON-fed typos easy to spot.
PluginConfig
Section titled “PluginConfig”Plugin-level enablement plus an arbitrary plugin-wide map.
const pluginConfig = PluginConfig( enabled: true, config: {'api_key': 'sk-demo'},);Fields
Section titled “Fields”| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | true | When false, Plugin.register is skipped and the plugin’s services do not enter that scope’s registry (global or session). |
config | Map<String, dynamic> | const {} | Plugin-wide values. Persisted in the settings model but not auto-injected into the plugin instance. |
The asymmetry with ServiceSettings.config is deliberate. Service configs flow
through the registry into PluginService.injectSettings. Plugin-level configs
live alongside the model for the host’s benefit (a place to stash API keys or
global toggles spanning several services), but reaching them is the host’s
job. Read PluginConfig.config from the application layer, or push the value
down onto a service the plugin registers and let the registry inject it.
copyWith({bool? enabled, Map<String, dynamic>? config}) follows standard
semantics: omitted parameters fall back to the current value.
ServiceSettings
Section titled “ServiceSettings”Per-slot configuration. Three knobs.
class ServiceSettings { final bool enabled; final Map<String, dynamic> config; final int? priority;
const ServiceSettings({ this.enabled = true, this.config = const {}, this.priority, });}Fields
Section titled “Fields”| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | true | When false, the registry emits a disable override for the slot. Resolution falls through to the next-highest-priority enabled registration. |
config | Map<String, dynamic> | const {} | Injected into PluginService.injectSettings on resolve. The service reads it via config (a ConfigNode). |
priority | int? | null | Optional override. Replaces the priority used at registration. null means “leave the registration default in place”. |
Higher priority wins resolution; the default registration priority is
Priority.normal (500). A priority override applies to the targeted
registration, not the slot, so two competing plugins on the same slot can
each carry independent overrides.
copyWith({bool? enabled, Map<String, dynamic>? config, int? priority})
follows standard semantics.
JSON round-trip
Section titled “JSON round-trip”All three settings types ship toJson() returning Map<String, dynamic> and
a matching fromJson(Map<String, dynamic>) factory. Round-tripping returns
an equal value:
/// Demonstrates the JSON round-trip assertion.void demonstrateJsonRoundtrip() { const settings = RuntimeSettings( plugins: { PluginId('main_agent'): PluginConfig(enabled: true), PluginId('experimental_router'): PluginConfig(enabled: false), }, ); final restored = RuntimeSettings.fromJson(settings.toJson()); assert(settings == restored);}The wire format mirrors the field shape:
{ "plugins": { "main_agent": {"enabled": true}, "experimental_router": {"enabled": false} }, "services": { "main_agent:agent.model": { "config": {"provider": "anthropic", "model": "claude-sonnet-4-5-20250929"} }, "*:agent.temperature": { "config": {"value": 0.7}, "priority": 200 } }}A few details:
RuntimeSettings.fromJsonrewraps each plugin key asPluginId(k). The JSON side stays plain strings; the in-memory side stays typed.ServiceSettings.toJsonomitsprioritywhen it isnull.ServiceSettings.fromJsonreadsprioritythrough(json['priority'] as num?)?.toInt(), so200and200.0both round-trip safely.- Missing
enabledkeys default totruein bothPluginConfig.fromJsonandServiceSettings.fromJson. Older payloads that omit the field still parse as enabled. - Missing
configmaps default toconst {}.
All three types use deep collection equality for == and hashCode.
ConfigNode
Section titled “ConfigNode”Typed read accessor over an injected settings map. The single const
constructor takes a Map<String, dynamic>, but you usually do not construct
one yourself: the registry passes a fresh ConfigNode to every resolved
service through injectSettings, which assigns it to PluginService.config.
Direct construction is fair game in tests and in code that handles settings
payloads outside the runtime.
Typed getters
Section titled “Typed getters”| Method | Returns | Behavior |
|---|---|---|
get<T>(key) | T? | Exact runtime-type match. No coercion. |
getString(key) | String? | Equivalent to get<String>(key). |
getInt(key) | int? | Accepts int, any num (via toInt(), truncating doubles), and String (via int.tryParse). |
getDouble(key) | double? | Accepts double, any num (via toDouble()), and String (via double.tryParse). |
getBool(key) | bool? | Accepts bool, the strings 'true' / 'false' (case-insensitive), and num where 0 is false and any other value is true. |
list<T>(key) | List<T>? | Returns value.cast<T>() when the value is a List and the cast succeeds. Returns null on cast failure or non-list value. |
map(key) | Map<String, dynamic>? | Returns value.cast<String, dynamic>() when the value is a map and the cast succeeds. Returns null on cast failure or non-map value. |
raw(key) | dynamic | Untyped passthrough. The value as stored. |
has(key) | bool | true only when the key exists and the value is non-null. |
There is no separate getMap accessor, and no path-style traversal.
The accessor is intentionally flat. To walk a nested map, read it with
map(key) and index into the result.
/// Demonstrates reading a nested map with map() then indexing.void demonstrateNestedMapAccess() { const node = ConfigNode({ 'headers': {'timeout_ms': 3000}, }); final headers = node.map('headers') ?? const {}; final timeoutMs = headers['timeout_ms'] as int?; print(timeoutMs);}Defaults and missing keys
Section titled “Defaults and missing keys”ConfigNode does not carry default values. The convention is to apply the
default at the call site through ??:
/// Demonstrates applying defaults via ?? at the call site.void demonstrateConfigNodeDefaults() { const node = ConfigNode({ 'temperature': 0.4, 'tools': ['search'], }); final temperature = node.getDouble('temperature') ?? 0.7; final tools = node.list<String>('tools') ?? const []; final provider = node.getString('provider') ?? 'openai'; print('$temperature $tools $provider');}map(key) follows the same null-on-miss contract as the other typed accessors.
If you want “missing map means empty map,” apply the fallback explicitly at the
call site:
final headers = config.map('headers') ?? const <String, dynamic>{};Coercion notes
Section titled “Coercion notes”The coercion in getInt, getDouble, and getBool is forgiving at the edge
so service code stays narrow when payloads arrive from JSON, env vars, or
form submissions. When your code controls the shape of the value, pass the
native type and stay out of the coercion path. A few sharp edges:
getInttruncates real-valued doubles.getInt('x')on1.9returns1.getDoubledoes not consumeintliterals viais double; it falls through to theis numbranch and callstoDouble(). The result is the samedouble, but the path through the function differs.getBoolmatches'true'/'false'case-insensitively but accepts no other string values.'yes','1', and'on'all returnnull.
Inspecting the node
Section titled “Inspecting the node”Iterable<String> get keys;bool get isEmpty;bool get isNotEmpty;keys returns the underlying map’s keys in insertion order. isEmpty and
isNotEmpty mirror the map’s own properties. Useful in tests, custom field
renderers, and code forwarding a config slice elsewhere.
ConfigNode is read-only
Section titled “ConfigNode is read-only”There are no setters. The class wraps a Map<String, dynamic> and only
exposes read accessors. The registry replaces the entire config field on
the service when settings change; it does not mutate the existing node.
To react to changed settings, override PluginService.onSettingsInjected(),
or Plugin.onPluginSettingsChanged(...) for plugin-wide reactions.
Treating config as a snapshot for the current resolution is safe; holding
a stale reference after a settings change returns old data but never panics.
ConfigNode.hashSettings
Section titled “ConfigNode.hashSettings”static String hashSettings(Map<String, dynamic> settings);Stable hex-encoded hash over a settings map, computed via deep collection
equality. The registry uses it to skip redundant injectSettings calls on
singleton and lazy-singleton services when the new map is structurally equal
to the previous one. Exposed so host code caching its own settings-keyed work
can use the same hash the registry does.