Skip to content

Config and permission overlays (multi-layer merge)

Permission and role behavior come from merging multiple config layers, not from a single file. Later layers override earlier ones; explicit permission overrides rules folded from legacy tools boolean maps. A single prompt can still narrow the tool set further; final closure happens at resolveTools and permission asks.

Roughly low to high:

flowchart TD
R[Remote / global config dir] --> F[Config file path override]
F --> P[Project agentforge config]
P --> A[In-dir agent / command scan]
A --> C[Config content env vars]
C --> H[Managed / account config]
H --> M[Managed policy highest]

Project side prefers .agentforge/ and agentforge.json(c); if disk only has a legacy directory layout, old paths remain readable so existing config is not split. Env vars use the AGENTFORGE_* family (including config path and content overrides).

MechanismMeaning
permissionallow / deny / ask, optionally with path or command patterns
tools (legacy)Boolean switches, folded into permission at load

Within one role: convert tools into permission first, then merge with explicit permission; explicit wins. Example: tools.bash=true and permission.bash=ask → ask. Global tools likewise fold into global permission. Runtime may also force overlays via permission-related env vars.

Global permission.bash=allow, project permission.bash=deny → project loads later → deny.

A rule value is either a single action or a map of “pattern → action.” A string means pattern: "*"; each key inside a nested object becomes one rule, matched by wildcard, and the last matching rule for a permission wins.

{
"permission": {
// reads always allowed
"read": "allow",
// writes denied by default, only this project’s reports directory is open
"edit": { "reports/**": "allow", "*": "deny" },
// shell auto-allows read-only commands, asks for everything else
"bash": { "git status": "allow", "git diff *": "allow", "*": "ask" },
// no subtasks
"task": "deny"
}
}

Config only supplies defaults. Within one session, a request can pass tools: { bash: false }, which folds into a deny("*") for bash, so the model sees no shell this turn; the next request without that field restores it.

RuleCan the model see the toolBehavior at execute time
deny with pattern *No; removed from this turn’s tool tableNever called
deny with a specific pattern (for example one directory)YesBlocked when a denied path is hit, with the reason written back
askYesA question goes out before execution; work continues after a decision

So “the model cannot see a tool” and “the tool got rejected” are different symptoms: the first is resolveTools plus whole-tool deny, the second is execute-time rule matching and the ask flow.

  • default_agent: primary role when none is specified
  • agent.{name}: model, prompts, step limits, permission, variants, and related fields
  • Legacy mode.* merges into same-named primary roles; mode doc scan results also merge into the agent table

Config only supplies defaults. Per-prompt tools input, session-level permission, and execute-time Permission.ask keep closing at runtime. Structured output does not appear from natural-language field descriptions; the API must pass format explicitly. See Processor and LLM adaptation.

flowchart LR
Cfg[Merged permission] --> RT[resolveTools]
In[Per-call input.tools] --> RT
RT --> LLM[Tools visible to model]
LLM --> Exec[execute]
Exec --> Ask[ask / deny / allow]

Check: after changing project config, confirm the instance reloaded for that directory; if files change without reload, the process may still hold the old merge. See Instance boundaries and concurrency.


Related reading: