fa-self-config · git:20260915.62da446 · 2026-09-15 · sha256 0e8ab4b767cdf3d3
fa-self-config git:20260915.62da446A
Immutable. This exact content is served forever at /api/v1/blob/0e8ab4b767cdf3d3.
---
name: fa-self-config
description: >
Configure fa (flutter_agent_harness) itself by editing the exact config
files the CLI settings commands write: provider/model selection and roles,
memory paths, tool availability, cubes, MCP servers, redaction, skills
access, approval and mode, themes and TUI appearance (tui.theme: switch
or create a theme). Use when asked to reconfigure fa without a human
in the REPL ("switch us to provider X", "point project memory at this
path", "disable web_search for this project", "switch the theme to
ohmypi").
when_to_use: Reconfiguring fa itself — settings, provider, models, memory,
tools, cubes, MCP, redaction, skills access, themes/TUI appearance — on
any host (CLI headless, REPL, app), by editing the YAML config the CLI
persists (themes also via ~/.fah/themes/*.json), never by guessing keys.
argument-hint: "[what to configure, e.g. 'point project memory at ./memory']"
allowed-tools:
- read
- write
- edit
- ls
- bash
- config
user-invocable: true
disable-model-invocation: false
---
# fa self-configuration
You are reconfiguring fa itself. The config file IS the source of truth: the
CLI settings commands (`/provider`, `/tools`, `/approval`, …) are thin
editors over the same YAML this skill edits. Edit the file, never invent
keys, verify after every edit, and report what changed and when it applies.
## Hard rules
1. **Never invent keys.** Unknown top-level keys are ignored silently; strict
sections throw `ConfigException` on unknown keys or bad value types and can
break startup — `mcp:`, `memory:`, `cube:`, `providerTimeouts:`, `skills:`,
`images:`, `models.custom` fields, `ttsr:`. The `tools:` section throws on
non-boolean values; unknown TOOL ids only warn (and absent capabilities can
never be force-enabled). Every key you write must come from this document.
2. **Never write API key VALUES into config.yaml.** Config carries key NAMES
(`apiKeyName:`) only. Values live in the OS secure store (`/key set`) or the
environment. A key value in YAML is a leaked secret.
3. **Preserve the file.** Edit surgically (the `edit` tool, or `write` only for
a file you fully own). Keep comments and unrelated keys byte-identical.
4. **Verify after every edit.** Run the `config` tool's `check` op (or
`fa config check`) and confirm it passes before declaring success. A
broken file must be detected by you, not discovered at next boot.
5. **Report scope + application.** Every change report states: which file, which
scope, and whether it applies live or at next boot.
## Config layout and precedence
| File | Scope | Notes |
|---|---|---|
| `~/.fah/config.yaml` | user (global) | main settings file; loaded by `loadCliConfig` |
| `<project>/.fah/config.yaml` | project | only the `memory:`, `cube:`, `tools:` sections are read from here — each wins over the user file |
| `<project>/.fah/rules.yaml` | project | TTSR stream rules; project rules win name clashes over the `ttsr:` section |
| `<project>/.fah/lsp.json` | project | LSP server map |
| `<project>/.fah/packages.yaml` | project | plugin configuration |
| `~/.dap/config.json` | user | DAP hub identity/channels (written by the `/settings` hub flow, not YAML) |
Project precedence is per section, not per file: `memory:`, `cube:` and
`tools:` are loaded separately from the project file
(`loadProjectMemoryConfig`, `loadProjectCubeSettings`, `loadProjectToolsConfig`)
and win over the same section in `~/.fah/config.yaml`. All other sections are
read from the user file only.
Project sections are strict: a present-but-invalid section throws
`ConfigException` at startup. A project `.fah/` that does not exist yet is
created with a minimal valid file — never leave a half-written YAML behind
(write to a temp name and rename, or write the full content in one `write`).
## Workflow
1. Read the request; identify the topic section below.
2. Inspect current state: `config` op `get <key>` / `path` (or read the two
config files — a missing file means defaults, not an error).
3. Make the edit: `config` op `set` (preferred — it validates before
writing and preserves the rest of the file byte-for-byte). Fall back to
a surgical file edit (rule 3) only for what `set` cannot express:
removing a key.
4. Verify: `config` op `check` (rule 4).
5. Report (rule 5), naming the CLI command equivalent that would have made
the same change.
## The config tool
The primary editing surface on every host — CLI, desktop app, web, iOS —
because it needs no shell and no `fa` process. One tool, four ops over the
same core the human CLI verbs wrap:
| op | args | effect |
|---|---|---|
| `check` | — | validate both config files with the real parsers; prints errors, warnings, notes and a final `config check: ok` / `config check: failed` |
| `path` | — | list the config file locations and whether each exists |
| `get` | `key` | the effective value of a dotted key; project scope wins for `memory`/`cube`/`tools` |
| `set` | `key`, `value`, `scope`? | surgical single-key write; the edited file is validated BEFORE the write, so an invalid value persists nothing |
`set` answers with the file, scope, old → new value, and whether the change
applies live or at next boot — echo that in your report (rule 5). `scope`
defaults by key: `memory`/`cube`/`tools` → the project file (created minimal
when absent), everything else → the user file; pass `global` or `project`
to override. Keys come from the topic sections below — the tool rejects
unknown top-level keys instead of writing them.
`set` also takes LIST-VALUED keys: pass the value as compact JSON (an
array or object) and it is rendered as a yaml block — `customProviders`
provider entries are settable this way, e.g. `config` op `set`, key
`customProviders`, value
`[{"name":"my-llama","apiType":"openai","baseUrl":"http://localhost:11434/v1","modelId":"llama3"}]`.
`get` reports list-valued keys as the same compact JSON, so the round
trip is get → edit the JSON → set. A whole list is replaced; to add one
entry, get first and re-set the extended list.
`get` of a platform-inapplicable key and `set` of one answer
`not applicable on this host` with the reason — never write there and
never suggest a file edit as a workaround. The stdio members of an MCP
server (`command`/`args`/`env`) need host process spawning: on web and
iOS/Android containers they are refused; configure a remote server via
`mcp.servers.<id>.url` instead. On a host with no home directory (web)
the global scope is refused for every user-file key — only the project
sections (`memory`/`cube`/`tools`) persist there.
Human/script equivalent (thin wrappers over the same service):
`fa config check`, `fa config path`, `fa config get <dotted.key>`,
`fa config set <dotted.key> <value> [--project|--global]`, and
`fa config export-providers [--out <file.fahx>]` (provider-preset export —
the one verb without a config-file write).
## Verification
`config` op `check` (or `fa config check`) validates both files with the
REAL section parsers and prints named diagnostics — this is the mandatory
final step of every edit:
- **What check reports as errors** (non-zero exit / `config check: failed`):
yaml syntax errors, strict-section schema errors (`mcp:`, `memory:`,
`cube:`, `tools:`, `providerTimeouts:`, `skills:`, `images:`, `roles:`,
`ttsr:`, `models.custom`, `fabric:`), and bad scalar types — each naming
file+section.
- **Warnings**: unknown top-level keys (the runtime silently ignores them;
the check does not — a typo must not survive) and dead project-file keys.
- **At next boot, semantic errors in strict sections are FATAL**:
`invalid ~/.fah/config.yaml: <named diagnostic>` — the process refuses to
start rather than boot a broken config. A passing check is what stands
between your edit and that.
- **YAML syntax errors remain silent at RUNTIME** (an unparseable file falls
back to defaults) — `check` is the only place they are reported, which is
why it is mandatory.
- **Live re-read surfaces**: in a running REPL, `/tools reload` and
`/mcp reload` re-read their sections immediately and print the parse
error instead of applying it — use them as the live check when a REPL
exists.
- Runtime objects keep the last good config when a reload fails (the failed
section never half-applies), so a loud reload error means "nothing changed".
## CLI parity map
Every settings command below has a config-file equivalent documented in this
skill. If a new settings command appears in the CLI without a section here,
this skill has rotted — flag it in your report.
| CLI | Section |
|---|---|
| `/provider`, `/providers` | [Provider & keys](#provider--keys) |
| `/model`, `/models`, `/model-edit` | [Models & roles](#models--roles) |
| `/memory` | [Memory](#memory) |
| `/tools` | [Tool availability](#tool-availability) |
| `/cube` | [Cubes](#cubes) |
| `/mcp` | [MCP servers](#mcp-servers) |
| `/redact` | [Redaction](#redaction) |
| `/skills` | [Skills access](#skills-access) |
| `/approval`, `/allow` | [Approval](#approval) |
| `/mode`, `/code`, `/architect`, `/review` | [Mode](#mode) |
| `/theme` | [Theme](#theme) |
| `/settings` | [Settings hub](#settings-hub) |
<!-- parity: /provider /providers /model /models /model-edit /memory /tools /cube /mcp /redact /skills /approval /allow /mode /code /architect /review /theme /settings -->
The `fa config check|path|get|set` verbs and the `config` tool share their
config-file equivalent with the topic sections above (see
[The config tool](#the-config-tool)); the parity test pins every verb
alongside the slash commands.
## Provider & keys
<!-- parity: /provider /providers -->
User file, top-level keys:
```yaml
provider: openai-completions # openai-completions | anthropic | google | dial | minimax | zai
model: openai/gpt-4o-mini # model id sent to the provider
baseUrl: https://openrouter.ai/api/v1
```
Saved custom endpoints (the `/provider custom` flow persists here):
```yaml
customProviders:
- name: my-llama # non-empty, unique
apiType: openai # openai | anthropic | google | dial | openrouter
baseUrl: http://localhost:11434/v1
modelId: llama3 # non-empty
keyName: MY_KEY # optional: env/secure-store NAME, never the value
authMethod: apiKey # apiKey | sso | jwt (optional)
```
Applies: a direct YAML edit takes effect at next boot. `/provider <name>
[baseUrl] [token]` switches the live session too.
Keys: never inline values (rule 2). Key resolution order — environment value,
then endpoint-scoped secure-store entry (`FA_KEY_<HOST>`), then legacy env-name
store entries. `/key set|delete` manages the store and has NO config.yaml
equivalent — that is deliberate.
## Models & roles
<!-- parity: /model /models /model-edit -->
User file. The active model is the `model:` top-level key (above). Optional
sections:
```yaml
roles: # intent → ordered fallback chain
default:
- openrouter/anthropic/claude-sonnet-4
- provider: openai # or a map entry:
model: gpt-4o
apiKeyName: OPENAI_API_KEY # optional; also baseUrl, contextWindow, maxTokens
smol: [openrouter/openai/gpt-4o-mini] # roles: default, smol, slow, plan, subagent, memory
modelOverrides: # scope chains to path prefixes
- path: ~/work/acme
roles:
plan: [anthropic/claude-opus-4-5]
retry: # chain fallback policy
retriesPerEntry: 2 # + baseDelayMs, maxBackoffMs, maxWaitMs, keyBackoffMs
providerTimeouts: # strict: only these two keys
connectTimeoutMs: 180000
streamIdleTimeoutMs: 300000
agent: # strict: only contextWindowCap (issue #273)
contextWindowCap: 262144 # owner context cap; min 16384 = the compaction reserve
images: # session image registry (strict): send-once
registry: true # kill switch — false = byte-for-byte legacy
maxPerRequest: 20 # per-request unique-image cap (default 20)
models: # media slots + named custom models
slots:
vision:
providerKind: openai-completions
baseUrl: https://api.openai.com/v1
modelId: gpt-4o
apiKeyName: OPENAI_API_KEY
custom:
fast:
provider: openai
baseUrl: https://api.openai.com/v1
model: gpt-4o-mini
contextWindow: 128000 # + maxTokens, input
```
Media slot names (exact): `imageGeneration`, `audioTts`, `musicGeneration`,
`videoGeneration`, `vision`, `transcription`. A slot without an override falls
back to the main connection.
CLI equivalents: `/model <id>` switches the active model (a
`models.custom.<name>` switches provider+endpoint+model in one step);
`/models config` displays this section, `/models set <slot> <model>
[baseUrl]` and `/models remove <slot>` edit `models.slots`;
`/model-edit [contextWindow|maxTokens <n>]` overrides token limits for the
session (persist per chain via `roles:` entries instead). Role edits apply at
next boot; `/model` applies live.
## Provider queue
Project or user file section; the `FA_PROVIDERS_QUEUE` env var carries
the same JSON list and WINS over both files (issue #418). The ordered
failover chain replaces the main model: the first healthy entry serves,
a dead provider hands over loudly to the next, the winner sticks for
the session (RAM-only, never persisted), and cooled-down entries
re-enter rotation:
```yaml
providersQueue:
- provider_type: openai-completions
provider_config:
model: moonshotai/Kimi-K2.6
baseUrl: https://gate.example/v1
apiKeyEnv: OPENAI_API_KEY
- provider_type: anthropic
provider_config:
model: claude-sonnet-4-6
apiKeyEnv: ANTHROPIC_API_KEY
```
`apiKeyEnv` names the ENV indirection — a key VALUE never lives in the
yaml. CLI equivalent: `/providers queue list|add|remove|move|test`;
edits persist to the winning scope's file and go live from the next
turn. App Settings carries the same editor (env scope is read-only
there).
## Memory
<!-- parity: /memory -->
The long-term memory location. `memory:` may live in the user file or the
project file — project wins.
```yaml
memory:
projectPath: ./memory # inside the repo; committed with it
userPath: ~/memory # or a user-level store
```
CLI equivalent: `/memory` shows stats, `/memory maintain` runs consolidation —
neither edits paths; the section is the only way to set them. The live memory
controller re-reads the section (project resolution via `loadProjectMemoryConfig`),
so a path change applies without restart.
## Tool availability
<!-- parity: /tools -->
`tools:` maps a tool id (or `mcp:<server>`) to on/off. Scopes, deepest wins
per key; the host's hard capability floor can never be overridden on:
1. global — `~/.fah/config.yaml` `tools:`
2. project — `<project>/.fah/config.yaml` `tools:`
3. session — `.tools/<sessionId>.yaml` next to the session file
4. runtime — `--tools` flag / `FA_TOOLS` env (this run only)
```yaml
tools:
web_search: false
mcp:
fs: true # nested mcp: == mcp:<server> keys
```
CLI equivalent: `/tools enable|disable <id> [global|project|session]` writes
the matching scope file; `/tools` lists every id with its deciding scope;
`/tools reload` re-reads all scopes live. Disabled tools are hidden from the
model and tombstoned at execution. Edits apply live after a reload.
## Cubes
<!-- parity: /cube -->
Sandbox profile selection. `cube:` may live in the user or project file
(project wins). Manifests live in `<project>/.fah/cubes/<name>.yaml`.
```yaml
cube:
enabled: true
config: .fah/cubes/strict.yaml # path to the cube manifest
```
CLI equivalent: `/cube use <name|path>` switches, `/cube off` disables,
`/cube list` lists manifests, `/cube reload` re-reads. Applies live via
`/cube`; a YAML edit applies at next boot (explicit `--cube`/`--cube-config`
flags beat both files).
## MCP servers
<!-- parity: /mcp -->
Strict section — unknown shapes throw `ConfigException`. User file only.
```yaml
mcp:
toolCallTimeoutMs: 60000
servers:
fs: # stdio server
command: npx
args: [-y, "@modelcontextprotocol/server-fs", /tmp]
env: {KEY: value}
docs: # remote server
url: https://example.com/mcp
transport: streamable-http # or sse
headers: {Authorization: "Bearer ${DOCS_TOKEN}"}
```
CLI equivalent: `/mcp` prints per-server status, `/mcp reload` re-reads the
config live (boot never blocks on MCP). Applies live after `/mcp reload`;
connects lazily in the background.
## Redaction
<!-- parity: /redact -->
User file `redact:` section (issue #24 layered pipeline):
```yaml
redact:
enabled: true
blockMode: false # deny credential-file reads outright
layers: # per-layer toggles (priority order in docs/redaction.md)
pii: true
allowlist: # regex patterns that survive redaction (git SHAs, UUIDs)
- "[0-9a-f]{40}"
toolAllow: [bash] # when non-empty, only these tools are redacted
toolDeny: [web_fetch] # never redacted; wins over toolAllow
```
CLI equivalent: `/redact on|off`, `/redact block on|off`, `/redact stats`,
`/redact layers`. The command toggles the live pipeline; the section is the
durable form written back when the CLI persists config. Redaction is
live-mutable — no restart needed either way.
## Compaction
Context-hiding engine choice (issue #148). User or project file (project
wins):
```yaml
compaction:
engine: structured # structured (default) | classic (rollback)
```
`structured` (the default since issue #287) hides records in place behind
one-line addressable markers (`compact_expand` restores any id or range
under a per-turn token budget); `classic` keeps the lossy summary
compaction — the legacy 1.0 rollback, selected through Settings →
Compaction (the app) or an explicit yaml/flag choice. Runtime override:
`fa --compaction-engine classic`; resolution is
flag > project > global > the structured default. Applies at
the next compaction.
## Skills access
<!-- parity: /skills -->
User file section governing third-party skill discovery
(`.claude/`, `.github/skills`, `.codex/`):
```yaml
skills:
access: granted # ask | granted | denied
disableShellExecution: false # strip !`cmd` injections from third-party skills
```
CLI equivalent: `/skills access ask|granted|denied` (and the `/skills` menu).
`/skills import` copies third-party skills into `.fah/skills`. Applies live on
re-discovery.
## Approval
<!-- parity: /approval /allow -->
User file top-level keys:
```yaml
approvalMode: yolo # always-ask | write | yolo | unattended
allowedTools: # always-allow list (exec tier still honors critical patterns per mode)
- web_fetch
```
CLI equivalent: `/approval <mode>`, `/allow <tool>` — both persist to these
keys. Applies at next boot (the running session keeps its live mode).
## Power
User file section (issue #325, reworked in #326). Sleep prevention for
live runs — by default ONE assertion PER RUN (acquired when the run
goes in flight, released when it settles), so the machine cannot
idle/sleep out from under a long run but CAN sleep while the agent
idles between turns:
```yaml
power:
sleepPrevention: idle # off | idle | display | system (default idle)
hold: per-run # per-run | session (default per-run)
```
Levels are cumulative: `idle` keeps the system from idle-sleeping
(macOS `caffeinate -i`), `display` also keeps the display awake
(`-i -d`), `system` also blocks AC system sleep and declares the user
active (`-i -d -s -u`). `hold: session` is the explicit opt-in that
holds from session start to exit instead (always-on deployments).
macOS spawns `caffeinate -w <fa pid>` (exits with fa — never
orphaned), Linux tries `systemd-inhibit` best-effort, other platforms
no-op. A failed assertion is a warning, never a crash.
CLI equivalent: `/power` (level + hold + held state). Applies at next
boot.
## Subagents heartbeat
User file section (issue #383). While a background subagent runs, the
parent receives a periodic status digest through the steering channel —
age, provider requests/tokens, last activity, and a
`healthy | stalled | no-progress-since-spawn` classification per running
child. A child with no provider activity for `stallMinutes` (or that
never issued a request at all) gets a loud `⚠ STALLED` flag with the
suggested `task_status → task_cancel → respawn` action; the flag
escalates to `⚠⚠` at twice the threshold:
```yaml
subagents:
heartbeatMinutes: 10 # digests cadence; 0 disables the heartbeat
stallMinutes: 20 # quiet window before a stall flag; 0 disables flags
```
Zero running children → no digest; completion notices are unchanged.
Config changes apply at the next digest tick — no restart required.
## Mode
<!-- parity: /mode /code /architect /review -->
User file top-level key:
```yaml
mode: code # code | architect | review
```
CLI equivalent: `/mode <name>` or the `/code`, `/architect`, `/review`
shortcuts — they switch live AND persist this key.
## Theme
<!-- parity: /theme -->
The TUI palette is data, never code. `/theme` lists every available theme
(built-ins first, then user themes) with a swatch preview; `/theme <name>`
switches immediately and persists the choice; `/theme reset` returns to
the default.
Config equivalent — the persisted theme name:
```yaml
tui:
theme: default # a built-in name or a user theme file stem
```
Built-in names: `default` (the historical site palette — teal `#5eead4`,
indigo `#818cf8`), `catppuccin`, `nord`, `dracula`, `ohmypi-dark`,
`ohmypi-light`, `pi`.
User themes live in `~/.fah/themes/<name>.json`; the file stem is the
theme name, and a user theme can never shadow a built-in (a clashing file
name is skipped with a boot note). The JSON holds a `"roles"` object mapping ROLE names to hex colors
(`"#rgb"` or `"#rrggbb"`): `accent`, `accent2`, `muted`, `highlight`,
`success`, `warning`, `error`, `border`, `focusBorder`, `userMessageBg`.
Every role is optional — a missing role inherits the default theme for
that slot. Unknown role names, invalid hex, or broken JSON produce a
named error (file, role, line) at load time, never a crash.
Applies: `/theme <name>` switches the live session in one frame; a direct
YAML edit takes effect at next boot. Asked to create a theme from a
description, write the JSON file, then apply it with `/theme <name>` and
confirm the swatch line with the user. Asked for a palette "like X" where
X is unknown, ask two or three palette questions first — never invent one
silently.
## Prompts, TTSR & A2A
The remaining user-file sections (all global scope):
```yaml
prompts: # prompt overrides: prompt id → text or @file
concise: "Answer in one short paragraph."
ttsr: # tool-schema reduction (TTSR)
enabled: true
contextMode: keep # keep | discard
repeatMode: after-gap # after-gap | once
a2a: # remote Agent2Agent endpoints
servers:
peer:
url: https://peer.example:8080
token: ${PEER_TOKEN} # env-resolved at boot — never a literal secret
fabric: # host discovery announcements (read-only:
capabilities: # written by hosts, never user-edited)
- name: yoclip.render
description: Render the open project to MP4
```
`a2a` servers are remote-only (a `url`, like remote MCP servers), so they
configure fine on process-less hosts. A `token` is an `${ENV}` reference
resolved at boot; config `check`/`set` keep the token text literal, so an
unset variable surfaces when the agent boots, not as a config error.
## Settings hub
<!-- parity: /settings -->
`/settings` is an interactive hub over the surfaces above (provider, model,
approval, cube, keys, MCP, DAP hub). It has no single config equivalent —
each arm edits the section documented for it. DAP hub settings are the one
surface NOT in config.yaml: the hub flow writes `~/.dap/config.json`
(identity keys, channels). Configure DAP through the hub flow or see
docs/dap.md; never hand-edit the DAP files while a hub client is running.
## Example invocations
- `point project memory at ./memory` → Memory section: `config` op `set`,
key `memory.projectPath`, value `./memory` (project file is the default
scope — created minimal if missing), then `check`, then report live
application.
- `switch us to provider X` → Provider & keys: `config` op `set` for
`provider:` (and `baseUrl:`) in the user file — global scope is the
default for these keys; report next-boot application and the `/provider`
equivalent.
- `add custom provider Y and switch to it` → Provider & keys: `config` op
`get customProviders`, `config` op `set customProviders <extended JSON
list>` (see the config-tool section), then `set provider openai-completions`,
`set model <entry modelId>`, `set baseUrl <entry baseUrl>`; verify with
`check`, report next-boot application and the `/provider` equivalent.
- `disable web_search for this project` → Tool availability: `config` op
`set`, key `tools.web_search`, value `false` (project scope by default);
report live after `/tools reload` semantics (host REPL) or next re-read.
- `configure cube backend Z` → Cubes: `config` op `set` for the `cube:`
keys (project file for a repo default) and reference the manifest in
`.fah/cubes/`.