CliConfig.fromYaml constructor
CliConfig.fromYaml(
- YamlMap map
Implementation
factory CliConfig.fromYaml(YamlMap map) {
final skillsSection = _parseSkillsSection(map['skills']);
// The power section (sleep-prevention level + hold lifecycle) is
// parsed once, strictly (issues #325/#326).
final powerSection = parsePowerSection(map['power']);
// The quota section (issue #823) is parsed once, strictly too.
final quotaSection = parseQuotaSection(map['quota']);
final agentSection = _parseAgentSection(map['agent']);
// The tui section (issue #805) is strict: theme, classic kill
// switch and statusLine parsed once, schema errors throw.
final tui = parseTuiSection(map['tui']);
return CliConfig(
// Issue #772: the persisted provider identity is the catalog KIND.
// Old name-shaped values (`chatgpt`, `chatgpt.com`) canonicalize on
// load — the file itself is rewritten only on the next save.
providerKind:
_canonicalSavedProvider(map['provider']) ?? 'openai-completions',
modelId: map['model'] as String? ?? 'openai/gpt-4o-mini',
baseUrl: map['baseUrl'] as String? ?? 'https://openrouter.ai/api/v1',
mode: map['mode'] as String? ?? 'code',
approvalMode: map['approvalMode'] as String? ?? 'yolo',
allowedTools: switch (map['allowedTools']) {
final YamlList list => [for (final entry in list) '$entry'],
_ => const [],
},
// The prompts section is parsed strictly: unknown prompt names throw
// [ConfigException] instead of silently doing nothing.
promptOverrides: parsePromptOverrideMap(map['prompts']),
// The roles section is parsed strictly: schema errors throw
// [ConfigException] instead of silently resetting to defaults.
modelRoles: map['roles'] == null && map['modelOverrides'] == null
? null
: ModelRolesConfig.fromYaml(map),
// The theme section: `tui.theme` names a built-in or user TUI theme;
// unknown names surface at boot (the default applies instead).
tuiTheme: tui.theme,
// The classic-chrome kill switch + statusLine section (issue #805);
// the renderer wires them in S3.
tuiClassic: tui.classic,
statusLine: tui.statusLine,
// The ttsr section is parsed strictly too (bad rules must surface).
ttsr: map['ttsr'] == null
? null
: TtsrConfig.fromYaml(map['ttsr'], sourcePath: '~/.fah/config.yaml'),
// The memory section: storage path overrides; strict like the rest.
memory: map['memory'] == null
? null
: MemoryConfig.fromYaml(map['memory']),
cube: map['cube'] == null ? null : CubeSettings.fromYaml(map['cube']),
// The tools section (capability-gated tool availability); strict too.
tools: map['tools'] == null ? null : ToolsConfig.fromYaml(map['tools']),
// The redact section (layered secret redaction); tolerant like
// RedactionConfig.fromJson — invalid values fall back to defaults.
redact: map['redact'] == null
? null
: RedactionConfig.fromYaml(map['redact']),
// The spills section (issue #678); tolerant by design — unknown
// keys and mistyped scalars become notes, never boot failures.
spills: map['spills'] == null
? null
: SpillsConfig.fromYaml(map['spills']),
// Saved custom providers; entry-level errors throw [ConfigException].
// Entries named after a built-in catalog provider are dropped (issue
// #221's ghost "openai"): they shadow `/provider <name>` routing and
// must never re-enter memory — a stale snapshot save would
// re-persist them after the user deleted them.
customProviders: switch (map['customProviders']) {
null => const [],
final YamlList list => [
for (final entry in list)
if (!_isGhostProviderEntry(entry))
CustomProviderEntry.fromYaml(entry),
],
final other => throw ConfigException(
'customProviders must be a list, got: $other',
),
},
// The models section (media slot overrides + custom model
// definitions) is parsed strictly too.
models: map['models'] == null
? null
: ModelsConfig.fromYaml(map['models']),
// The mcp section (external tool servers) is parsed strictly too.
mcp: map['mcp'] == null ? null : McpConfig.fromYaml(map['mcp']),
// The a2a section (remote Agent2Agent endpoints) is strict too; its
// `${NAME}` token references resolve against the environment.
a2a: map['a2a'] == null
? null
: A2aConfig.fromYaml(
map['a2a'],
(name) => Platform.environment[name],
),
// The providerTimeouts section (provider watchdog overrides) is strict
// too.
providerTimeouts: parseProviderTimeouts(map['providerTimeouts']),
// The compaction section (engine selector, issue #148; judge
// budget knob, issue #541) is strict: a typo throws instead of
// silently running the classic engine or restoring the default
// 300s budget.
compactionEngine: CompactionEngine.fromSection(
map['compaction'],
label: '~/.fah/config.yaml',
),
compactionJudgeBudgetSeconds: parseJudgeBudgetSeconds(
map['compaction'] is Map
? (map['compaction'] as Map)['judgeBudgetSeconds']
: null,
label: '~/.fah/config.yaml',
),
// The trajectory section (issue #385) is a plain boolean today:
// `wireDump` opts the raw outbound payloads into the session ledger.
wireDump: _parseTrajectorySection(map['trajectory']),
images: parseImagesSection(map['images']),
powerSleepPrevention: powerSection.sleepPrevention,
powerHold: powerSection.hold,
// The quota section (issue #823): badge opt-in + cache TTL, strict.
quota: quotaSection,
// The agent section (owner-side context cap + mode, issues
// #273/#679/#680) is strict too.
contextWindowCap: agentSection?.contextWindowCap,
stuckTool: agentSection?.stuckTool,
agentMode: agentSection?.agentMode,
agentLoadMode: agentSection?.mode,
misuseBreaker: agentSection?.misuseBreaker ?? true,
// The subagents section (background-subagent heartbeat, issue #383)
subagents: SubagentsConfig.fromYaml(map['subagents']),
waiting: WaitingConfig.fromYaml(map['waiting']),
jobs: JobsConfig.fromYaml(map['jobs']),
// The fabric section (issue #27 phase 2 discovery announcements) is
// strict too.
fabric: map['fabric'] == null
? null
: FabricConfig.fromYaml(map['fabric']),
// The links section (issue #691) is strict on shape, tolerant on
// unknown keys (notes, never boot failures — AC7).
links: map['links'] == null
? const LinksConfig()
: LinksConfig.fromYaml(map['links']),
// The skills section (third-party skills access consent + shell
// execution toggle + per-skill on/off entries) is strict too.
skillsAccess:
skillsSection['skillsAccess'] as SkillsAccess? ??
SkillsAccess.granted,
skillsDisableShellExecution:
skillsSection['skillsDisableShellExecution'] as bool? ?? false,
skillToggles:
skillsSection['skillToggles'] as Map<String, bool>? ?? const {},
// The output section (gh-1198) is strict: `streamThinking` opts
// line-mode/headless runs into the live dimmed thinking stream;
// absent = false = the byte-identical legacy output.
streamThinking: _outputStreamThinking(map['output']),
);
}