Profiles define the agent's identity, tools, and behaviour for a specific domain.
navi/profiles/base.py)Each profile is loaded from a directory under navi/profiles/<id>/:
config.json — all fields belowsystem_prompt.txt — domain-specific instructionssubagent_system_prompt.txt — injected into subagents spawned from this profile (optional)config.json fields| Key | Type | Default | Description |
|---|---|---|---|
id |
str | required | Unique profile identifier (matches directory name) |
name |
str | required | Human-readable name shown in UI |
description |
str | required | Longer description shown in profile picker |
short_description |
str | "" |
One-line summary injected into every profile's system prompt (cross-profile awareness) |
full_description |
dict | {} |
Structured dict: specialization, when_to_use, key_tools keys |
| Key | Type | Default | Description | |
|---|---|---|---|---|
llm_backend |
str | "ollama" |
Backend key: "ollama", "openai" |
|
model |
str or list[str] | ["gemma4:31b-cloud"] |
Model priority list — first available wins. String is accepted and auto-wrapped. | |
temperature |
float | 0.7 |
Sampling temperature for main loop calls | |
max_iterations |
int | 10 |
Hard cap on tool-calling iterations per turn | |
top_k |
int \ | None | None |
Ollama sampling top_k |
top_p |
float \ | None | None |
Ollama sampling top_p |
num_thread |
int \ | None | None |
CPU threads for local inference. None = Ollama default |
| Key | Type | Default | Description |
|---|---|---|---|
tools |
ToolConfig |
{} |
Required. Explicit tool configuration with two scopes: agent and subagent. Each scope has native: list[str] (built-in and user tool names) and mcp: dict[str, list[str]] (MCP server groups). |
tools.agent controls what the main loop sees. tools.subagent controls what sub-agents spawned from this profile see. If tools.subagent is empty, it falls back to tools.agent.
spawn_agent may receive an optional profile_id. If omitted, the subagent uses the parent session's current profile. If provided, the subagent uses the selected profile's model, prompt, planning flags, and tools.subagent fallback.
Inside tools.{agent,subagent}.mcp, each key is an MCP server name and each value is a list of group names (or "*" for all groups). Named groups resolve to concrete tools via the server's config in mcp_servers.d/. Example:
{
"mcp": {
"navi-web": ["search", "browse", "request"]
}
}
This exposes the tools mcp__navi-web__web_search, mcp__navi-web__web_view, and mcp__navi-web__http_request. * expands to every tool advertised by that server.
Older configs used enabled_tools, subagent_tools, and mcp_servers as flat top-level fields. The loader still auto-migrates them into tools.agent / tools.subagent for backward compatibility, but new profiles should use the explicit tools structure.
When tools.subagent is non-empty, only MCP groups listed there are exposed to the sub-agent. This prevents a profile's main MCP servers from leaking into restricted sub-agent contexts.
| Key | Type | Default | Description |
|---|---|---|---|
think_enabled |
bool | true |
Pass think=True to LLM on every call (extended reasoning). Disable for latency-sensitive profiles. |
iteration_budget_enabled |
bool | true |
Inject remaining iteration count into context so the model knows when to wrap up. |
goal_anchoring_enabled |
bool | true |
Inject a goal-reminder system message every N iterations to prevent drift. |
goal_anchoring_interval |
int | 5 |
N for goal anchoring. |
anti_stall_enabled |
bool | true |
Detect looping without todo progress and inject a hard warning. |
anti_stall_threshold |
int | 8 |
Consecutive iterations without progress before stall warning fires. |
step_validation_enabled |
bool | false |
Reserved flag — todo validation is unconditional in the current implementation. |
One flag keeps an autonomous agent inside the user's literally requested scope. Defaults false (legacy "free flight" behavior — explore broadly, finish discovered work). Enabled on navi_code.
| Key | Type | Default | Description |
|---|---|---|---|
scope_boundary_enabled |
bool | false |
Inject a standing [Scope boundary] system message telling the agent to act strictly within the requested scope — do not expand to sibling/parent directories or projects, and do not execute discovered TODO/roadmap/milestone/backlog docs unless explicitly asked. Also gates the memory-facts scope filter (see below). |
The memory-facts scope filter: when scope_boundary_enabled is on, _memory_facts_msg drops memory facts whose value is an absolute path outside the session cwd (e.g. a stale project_root pointing at another project). Facts inside the session cwd and non-path facts are kept. No facts are deleted — only injection is filtered. See docs/mechanics.md.
Top-level planning is agent-invoked: the agent calls the plan tool when a task warrants it (add it to tools.agent.native). There is no pre-turn gate — free conversation and trivial tasks execute immediately. The pipeline has two phases:
COMPLEXITY: simple | medium | complex. Sub-agents can output DIRECT to skip planning for trivial subtasks (the shortcut is never offered at top level).TOOL / AGENT / SELF and uses adaptive plan depth.When the plan is ready, the tool result tells the agent what to do next based on COMPLEXITY: complex — present the plan to the user and wait for confirmation; otherwise — proceed with execution. A re-plan is the same tool called with a reason (and optional updated_goal); it packs the current todo and scratchpad findings into the planning context and replaces the todo.
| Key | Type | Default | Description |
|---|---|---|---|
planning_phase1_enabled |
bool | true |
Enable Phase 1 (task analysis). When disabled, Phase 3 runs without analysis context. |
planning_phase3_enabled |
bool | true |
Enable Phase 3 (structured execution plan). When disabled, only Phase 1 (analysis) runs. |
| Key | Type | Default | Description | |
|---|---|---|---|---|
subagent_think_enabled |
bool \ | None | None |
Extended reasoning for sub-agents. None = inherit think_enabled from parent profile. |
subagent_planning_enabled |
bool | false |
Sub-agents spawned from this profile also run the planning pipeline before their tool loop. | |
context_providers |
list[str] | [] |
Extra context providers to inject for this profile (by name). Global providers are always injected. | |
is_admin_only |
bool | false |
If true, the profile may only be used by a user whose role is admin: it is absent from every profile list and from the injected "Available profiles" block, cannot be selected when a session is created, cannot be switched to, and cannot be spawned. Enforced by admin_only_blocked() in navi/profiles/base.py — the one place the flag/role rule lives. Set in config.json; a profile_overrides row written by PATCH /admin/profiles/{id}/availability is applied on top at startup and wins. |
|
is_subagent_only |
bool | false |
If true, profile can only be used via spawn_agent; switch_profile is blocked. Useful for narrow specialist agents that should never become the main session profile. |
Per-profile overrides for context compression. When set, they take precedence over the global settings (CONTEXT_KEEP_RECENT, CONTEXT_SUMMARY_MAX_TOKENS) inside compress_context / compress_session and the summary system prompt. The active profile is propagated to the compressor via Agent.set_profile() and to the post-turn CompressionWorker via WorkerContext.profile.
| Key | Type | Default | Description | |
|---|---|---|---|---|
compression_keep_recent |
int \ | None | None |
Override CONTEXT_KEEP_RECENT — number of recent turns kept verbatim. navi_code uses 12. |
compression_max_tokens |
int \ | None | None |
Override CONTEXT_SUMMARY_MAX_TOKENS — max output tokens for the summary LLM call. |
compression_prompt_file |
str \ | None | None |
Extra instructions appended to the summary system prompt (loaded from the profile dir). |
See sessions.md for the full compression algorithm and mechanics.md for the catalog.
| Key | Type | Default | Description |
|---|---|---|---|
subagent_system_prompt |
str | "" |
Inline sub-agent system prompt. If empty, the loader reads subagent_system_prompt.txt from the profile directory. Prepended on top of the parent's system_prompt when inherit_system_prompt=True. |
AgentProfile uses model_config = {"extra": "allow"}, so unknown keys in config.json are accepted and preserved (round-tripped through the admin panel's GET/PUT) rather than rejected. This lets profiles carry custom metadata without code changes. |
Seven of the ten registered profiles are admin-only (is_admin_only: true): the owner's own working set. A user whose role is user neither sees nor can reach them.
| ID | Name | Audience | Models (priority order) | Temp | Planning |
|---|---|---|---|---|---|
assistant |
Assistant | user | glm-5.3-flash:cloud → gemma4:31b-cloud → qwen3.5:397b-cloud → kimi-k2.6:cloud → gemma4:26b-a4b-it-q4_K_M → qwen3.6:27b |
0.45 | Yes |
designer_3d |
3D Designer | user | glm-5.3-flash:cloud → gemma4:12b-it-qat-128k → gemma4:31b-cloud → qwen3.5:397b-cloud → qwen3.6:35b → gemma4:26b-a4b-it-q4_K_M |
0.35 | Yes |
coder |
Coder | user | glm-5.3-flash:cloud → gemma4:31b-cloud → qwen3.5:397b-cloud → kimi-k2.6:cloud → gemma4:26b-a4b-it-q4_K_M → qwen3.6:27b |
0.30 | Yes |
secretary |
Personal Secretary | admin | glm-5.3-flash:cloud → gemma4:31b-cloud → gemma4:12b-it-qat-128k → qwen3.5:397b-cloud → kimi-k2.6:cloud → gemma4:26b-a4b-it-q4_K_M → qwen3.6:27b |
0.45 | Yes |
server_admin |
Server Administrator | admin | glm-5.3-flash:cloud → gemma4:31b-cloud → gemma4:12b-it-qat-128k → qwen3.5:397b-cloud → gemma4:26b-a4b-it-q4_K_M → kimi-k2.6:cloud → qwen3.6:27b |
0.25 | Yes |
developer |
Developer | admin | glm-5.3-flash:cloud → gemma4:31b-cloud → gemma4:12b-it-qat-128k → qwen3.5:397b-cloud → gemma4:26b-a4b-it-q4_K_M → kimi-k2.6:cloud → qwen3.6:27b |
0.35 | Yes |
discuss |
Discussion | admin | glm-5.3-flash:cloud → gemma4:12b-it-qat-128k → gemma4:31b-cloud → qwen3.5:397b-cloud → kimi-k2.6:cloud → qwen3.6:27b |
0.65 | Phase 1 only |
modeler_3d |
3D Modeler | admin | glm-5.3-flash:cloud → gemma4:12b-it-qat-128k → gemma4:31b-cloud → qwen3.5:397b-cloud → kimi-k2.6:cloud → gemma4:26b-a4b-it-q4_K_M → qwen3.6:35b |
0.35 | Yes |
navi_code |
Navi Code | admin | glm-5.3-flash:cloud → gemma4:31b-it-qat → gemma4:26b-a4b-it-qat → gemma4:12b-it-qat-128k → gemma4:31b-cloud |
0.35 | Yes |
dispatcher |
Synapse Reaction Dispatcher | hidden | glm-5.3-flash:cloud → gemma4:31b-cloud → qwen3.6:35b → gemma4:26b-a4b-it-q4_K_M |
0.20 | No |
Model chains are swapped as models come and go — navi/profiles/<id>/config.json is the source of truth. The dispatcher profile is additionally is_hidden and never appears in the picker.
All profiles share a base tool set. User tools from tools/enabled.json are merged in at runtime for every profile, admin or not — see "Restricted profiles" below.
assistant, designer_3d, coderThese three are the profiles an ordinary user gets. They exist to keep a household user inside the Navi server's own working directory and out of the rest of the owner's infrastructure, while still being useful for everyday work. All three carry the same native tool set — they differ in system prompt, model and MCP groups.
Deliberate omissions, none of which appears in any of the three (or in their tools.subagent, or in their MCP groups):
| Tool | Why it is withheld |
|---|---|
ssh_exec |
Reaches any host, takes a password inline, host-key checking off. |
peer |
Runs an agent on a neighbouring machine, where PEER_ASK_PROFILE=server_admin. |
reload_tools, create_mcp_server |
Execute arbitrary code on the Navi host. |
test_mcp_tool |
Calls any tool of any server, bypassing the profile's MCP groups — it would undo both the group limits and the BYOK rule. |
One tool goes the other way: image_view is granted to all three, so the 3D designer can inspect the renders mcp__navi-3d__render_stl produces instead of shipping geometry it has never looked at. It reads any absolute path and fetches any http(s) URL, subject to a raster-only extension filter (.jpg, .jpeg, .png, .gif, .webp, .bmp) — it cannot read .env or any other text file, but it can read any image on the host. See the residual risk below; the boundary instruction in the three system prompts ("pass only session paths and the paths the 3D server returned") is a prompt rule, not a code check.
MCP groups are all read-only or local:
assistant — navi-web (search, browse), navi_ui (ui), and read-only use of the owner's knowledge and tracking servers: gnexus-book (read), gntodo (read), synapse (read), hard-panel (read).designer_3d — navi-3d (modeling, analysis), navi-web (search, browse), navi_ui (ui).coder — navi-web (search, browse), gnexus-book (read), gntodo (read).The navi-web request group (http_request — arbitrary method, headers and body, issued from the Navi host) is given to none of them, and gnexus-creds and tgclient are not given at all, not even read-only.
Because MCP servers with a user_key slot require the user's own credential, those groups only take effect once the user has saved a key in Settings → MCP keys; without one the server is not offered to them at all. navi-3d, navi-web and navi_ui have no slot and are always available.
Two consequences worth knowing:
switch_profile and spawn_agent are in all three profiles, and none of the three is admin-only — so within a user session the agent can move between them, and the native tool surface a user actually has is the union of the three. The native sets are kept identical for exactly that reason.Known and accepted by the owner (2026-10-09). These are recorded so that nobody reads the profile list above as a stronger guarantee than it is:
terminal and code_exec run as the Navi process; python3 script.py from a restricted profile can read /home/ubuntu/navi-1/.env. The only real defence is the model obeying the prompt.ssh_exec, peer, reload_tools, create_mcp_server and test_mcp_tool still check no role; they are withheld from the restricted profiles by composition alone. A model that talks itself into "remembering" one, or finds its description through tool_manual, still cannot call it — it is not in the list.image_view is granted deliberately, and is the one file reader with no path check. It accepts any absolute path or URL whose extension is a raster image type, so it cannot read .env, source or logs, but it can read any image file on the host. It is in all three sets because the 3D designer needs to see its own renders, and because switch_profile would hand it to the other two regardless. The prompt asks for session paths only; nothing enforces that.gmail is the exception, and is not covered by composition. The global file tools/enabled.json is merged into every profile's tool list by build_tool_list (navi/core/tool_utils.py), so gmail — Navi's own mailbox, not a personal one — is reachable from all three restricted profiles regardless of their tools.agent.native. The owner accepted this as no risk.switch_profile gives the union of the three sets, as described above.test_mcp_tool is why BYOK and the MCP groups hold only for profiles that lack it. Its absence from the restricted profiles is load-bearing.user_data/ is tracked in git and holds files that were not meant to be distributed; the owner deferred cleanup.navi_codeTerminal-first local coding assistant. Designed for the Navi Code CLI and single-user local deployments:
todo, scratchpad, reflect, plan, switch_profile, list_profiles, filesystem, code_exec, terminal, image_view, ssh_exec, memory, list_tools, tool_manual, spawn_agent, schedule_recall, manage_recall.navi-web (search, browse, request) — web lookup and page browsing.share_file, content_publish, gmail.plan tool is in the native tool set, so the agent plans deliberately when a task warrants it. Phase 1 classifies COMPLEXITY: simple | medium | complex — complex tasks get "present the plan and wait for confirmation".scope_boundary_enabled is true — the agent stays within the requested scope (does not climb to sibling projects or execute discovered milestone/TODO docs). Flip it off to reproduce the legacy "free flight" behavior.permissions.md (designed, not yet implemented). The old prompt-level "confirm before destructive ops" nudge and the client-side permission dialog were removed.Use it with NAVI_DEFAULT_PROFILE_ID=navi_code so POST /sessions without a profile_id creates a navi_code session automatically. See docs/navi_code.md for the full local-terminal setup.
The LLM sees (injected fresh on every call, never stored in session):
{persona.txt content}
---
{profile system_prompt.txt content}
persona.txt — global layer: personality, CONTEXT FIRST principle, self-extension rules, scratchpad/todo/memory instructions, delegation rules.
system_prompt.txt — domain layer: tool priorities, workflow, safety rules for this profile.
navi/profiles/my_profile/config.json (minimal example):
{
"id": "my_profile",
"name": "My Profile",
"description": "...",
"short_description": "...",
"model": ["gemma4:31b-cloud", "gemma4:26b-a4b-it-q4_K_M"],
"temperature": 0.5,
"max_iterations": 20,
"tools": {
"agent": {
"native": ["todo", "scratchpad", "plan", "filesystem", "terminal"],
"mcp": {
"navi-web": ["search"]
}
},
"subagent": {
"native": ["todo", "filesystem", "terminal"],
"mcp": {}
}
},
"planning_phase1_enabled": true,
"planning_phase3_enabled": true,
"think_enabled": true,
"iteration_budget_enabled": true,
"goal_anchoring_enabled": true,
"goal_anchoring_interval": 5,
"anti_stall_enabled": true,
"anti_stall_threshold": 8,
"step_validation_enabled": false,
"scope_boundary_enabled": false,
"subagent_planning_enabled": false
}system_prompt.txt with domain-specific instructions.subagent_system_prompt.txt."is_admin_only": true (see the field table above); a profile without it is offered to every account with role user.A prompt that documents a tool with tool_manual("<name>") or a manuals/<name>.md path must name something that exists — tests/unit/tools/test_manual_drift.py walks navi/profiles/**/*.txt and fails on a citation that resolves to nothing.
switch_profile tool repoints the session with one narrow UPDATE sessions SET profile_id (PgSessionStore.set_profile() — never a full save(), which would hand out sequence numbers the running turn is still claiming). After each tool execution batch, run_stream() compares the stored profile against the profile the run is bound to — not against session.profile_id, since the store's write leaves those two equal again — and reloads profile + tools when they differ. Takes effect on the next LLM call. For the same reason save() leaves the profile_id column alone on conflict: a mid-run save carries the profile the run started with and must not write a switch back.
Rules (in persona): don't switch for a single off-topic question; switch when the domain clearly changes; never switch back and forth repeatedly.