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.
5.5 KiB
Profiles
Profiles let you keep multiple independent gnoma configurations and switch between them. Common cases:
workvs.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):
- Built-in defaults.
~/.config/gnoma/config.toml— the base config.~/.config/gnoma/profiles/<name>.toml— the active profile (only whenprofiles/exists).<projectRoot>/.gnoma/config.toml— project overrides.- 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_profilefrom the baseconfig.tomlis 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:
default_profile = "work"
# Settings here apply to every profile unless the profile overrides them.
[tools]
bash_timeout = "30s"
~/.config/gnoma/profiles/work.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:
[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:
[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
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 byid. Profile entries override the matching base entry; new IDs append. So a profile can tweak one arm'scost_weightwithout redeclaring the rest.[[mcp_servers]]: merged byname(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:
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.tomlas 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 theprofiles/directory to create.
Existing single-config installations don't need to do anything.