Files
gnoma/docs/profiles.md
T
vikingowl 635dad660c feat(config): per-profile config layering with --profile flag (Phase C-1)
Adds opt-in user profiles for swapping API keys, CLI binaries, and
permission modes between contexts (work/private/experiment/...).

Profile mode engages only when ~/.config/gnoma/profiles/ exists, so
existing single-config installations are untouched. Selection order:
--profile flag → default_profile in base config → fatal error.

Layering: defaults → ~/.config/gnoma/config.toml → profiles/<name>.toml
→ <projectRoot>/.gnoma/config.toml → env. Map sections merge per-key;
[[arms]] and [[mcp_servers]] merge by id/name; [[hooks]] appends.

Per-profile data: quality-<name>.json and sessions/<name>/ keep the
bandit and session list from cross-contaminating between profiles.

Profile names restricted to [A-Za-z0-9_-] to block --profile=../foo
path traversal into derived paths.
2026-05-19 21:35:33 +02:00

197 lines
5.5 KiB
Markdown

# Profiles
Profiles let you keep multiple independent gnoma configurations and switch
between them. Common cases:
- `work` vs. `private` — different API keys, different CLI binaries,
stricter or looser permission mode per context.
- `experiment` — a non-default SLM model, plan mode, no persistence.
Profile mode is opt-in: gnoma stays on its single-config behavior until
you create `~/.config/gnoma/profiles/`.
## Layout
```
~/.config/gnoma/
├── config.toml # base settings + default_profile
├── profiles/ # opt-in directory; presence enables profile mode
│ ├── work.toml
│ ├── private.toml
│ └── experiment.toml
├── quality-work.json # per-profile router quality data
├── quality-private.json
└── quality-experiment.json
```
Per-project, session storage segregates the same way:
```
<projectRoot>/.gnoma/sessions/
├── work/
├── private/
└── experiment/
```
## Loading order
Each `gnoma` invocation merges configuration in this order (lowest to
highest priority):
1. Built-in defaults.
2. `~/.config/gnoma/config.toml` — the **base** config.
3. `~/.config/gnoma/profiles/<name>.toml` — the **active** profile
(only when `profiles/` exists).
4. `<projectRoot>/.gnoma/config.toml` — project overrides.
5. Environment variables (`ANTHROPIC_API_KEY`, `GNOMA_PROVIDER`, etc.).
The active profile is resolved as follows:
- If `--profile <name>` is passed on the CLI, that wins.
- Otherwise, `default_profile` from the base `config.toml` is used.
- If neither is set and `profiles/` exists, gnoma fails fast with a
list of available profiles. (Silent fallback to defaults would hide
configuration mistakes.)
## Example: base + two profiles
`~/.config/gnoma/config.toml`:
```toml
default_profile = "work"
# Settings here apply to every profile unless the profile overrides them.
[tools]
bash_timeout = "30s"
```
`~/.config/gnoma/profiles/work.toml`:
```toml
[provider]
default = "anthropic"
[provider.api_keys]
anthropic = "${ANTHROPIC_WORK_KEY}"
[cli_agents]
claude = "claude-work"
[permission]
mode = "default"
[slm]
backend = "ollama"
model = "reecdev/tiny3.5:1.5b"
```
`~/.config/gnoma/profiles/private.toml`:
```toml
[provider]
default = "openai"
[provider.api_keys]
openai = "${OPENAI_PRIVATE_KEY}"
[cli_agents]
claude = "claude-priv"
[permission]
mode = "auto"
[slm]
backend = "ollama"
model = "reecdev/tiny3.5:500m"
```
`~/.config/gnoma/profiles/experiment.toml`:
```toml
[provider]
default = "mistral"
model = "mistral-large-latest"
[permission]
mode = "plan"
[slm]
enabled = false # turn the classifier off entirely
[session]
max_keep = 0 # don't keep session history for experiments
```
## Switching
```bash
gnoma --profile work providers # use work profile
gnoma --profile private # private profile, default subcommand (TUI)
gnoma # base default_profile (here: work)
```
Profile selection is per-invocation. Restart re-reads `default_profile`;
no "last used" persistence — explicit switches stay explicit.
## Merge semantics
- **Scalars** (`provider.default`, `provider.model`, `tools.bash_timeout`,
…): the profile value wins if set; otherwise base is preserved.
- **Maps** (`provider.api_keys`, `provider.endpoints`, `cli_agents`,
`rate_limits`): per-key merge. Profile overrides individual keys
without erasing the rest.
- **`[[hooks]]`**: profile hooks are appended after base hooks.
- **`[[arms]]`**: merged by `id`. Profile entries override the matching
base entry; new IDs append. So a profile can tweak one arm's
`cost_weight` without redeclaring the rest.
- **`[[mcp_servers]]`**: merged by `name` (same policy as arms).
- **`[security]`**, **`[plugins]`**, etc.: profile replaces if the
profile defines anything in that section.
The project-level `.gnoma/config.toml` layer applies on top of the
merged base+profile result. Environment variables apply last and
override everything.
## Profile name rules
Names must match `[A-Za-z0-9_-]+`. Dots, slashes, spaces, and other
characters are rejected to keep derived paths
(`quality-<name>.json`, `sessions/<name>/`) predictable and to prevent
path traversal via `--profile`.
## Where per-profile data lives
| Data | Path |
|---|---|
| Router quality (bandit telemetry) | `~/.config/gnoma/quality-<profile>.json` |
| Session history | `<projectRoot>/.gnoma/sessions/<profile>/` |
| Plugins | `~/.config/gnoma/plugins/` (shared across profiles) |
| Skills | `~/.config/gnoma/skills/` (shared across profiles) |
Plugins and skills stay global on purpose — they're code, not
preferences. Use profile-specific `[plugins].enabled` / `disabled`
lists if you need a different mix per profile.
## `gnoma router stats` and profiles
When a profile is active, `gnoma router stats` reads
`quality-<profile>.json` and prefixes its output with the profile name
so it's clear which dataset you're looking at. To compare profiles:
```bash
gnoma --profile work router stats
gnoma --profile private router stats
```
## Backward compatibility
If `~/.config/gnoma/profiles/` does not exist, gnoma behaves exactly
as before:
- Reads `~/.config/gnoma/config.toml` as the only base config.
- Stores quality data at `~/.config/gnoma/quality.json`.
- Stores sessions at `<projectRoot>/.gnoma/sessions/` (no profile
subdirectory).
- `--profile <name>` returns a clear error pointing you at the
`profiles/` directory to create.
Existing single-config installations don't need to do anything.