What happened?
The [agents.presets.*] config section with tools.allow / tools.deny looks like it should configure the main agent session's tool policy. The [agents] default_preset = "..." field reinforces that impression — "default" suggests "applies to the default agent, i.e. the main session." Users configuring a deny list under [agents.presets.full] tools.deny = ["browser", "web_fetch", ...] and setting default_preset = "full" naturally expect those tools to be hidden from the main agent.
They are not. The preset system applies ONLY to sub-agents spawned via the spawn_agent tool. The main session's tool filtering reads flat [tools.policy], which is a completely separate config section. An empty or absent [tools.policy] means the main session gets no tool filtering at all, even when [agents.presets.full] tools.deny is populated.
This is not a code bug — the preset system works as implemented. But the naming, the template documentation, and the absence of any validation warning combine to create a trap where users configure tool hiding and silently get zero enforcement on the session that matters most.
We fell into this trap and ran for 2 weeks with a deny list that was silently no-op for the main agent. After discovering the gap, we had to add a top-level [tools.policy] section to our generator.
Evidence
Only consumer of default_preset: crates/tools/src/spawn_agent.rs L193:
let preset_name = explicit_name.or_else(|| agents.default_preset.clone());
The preset selection code lives entirely inside the spawn_agent tool. Grep for default_preset across the codebase confirms: the only non-test, non-schema consumers are spawn_agent.rs L193, L667, L671 — all inside the spawn_agent tool implementation.
Main session uses flat [tools.policy]: crates/chat/src/lib.rs L1431-L1443, effective_tool_policy:
fn effective_tool_policy(config: &moltis_config::MoltisConfig) -> ToolPolicy {
let mut effective = ToolPolicy::default();
if let Some(profile) = config.tools.policy.profile.as_deref() ...
let configured = ToolPolicy {
allow: config.tools.policy.allow.clone(),
deny: config.tools.policy.deny.clone(),
};
effective.merge_with(&configured)
}
This reads config.tools.policy.* exclusively. It never consults config.agents.presets.*. apply_runtime_tool_filters at L1445 calls it per turn for the main session.
AgentPreset doc comment is accurate but buried: crates/config/src/schema.rs L345-L378. The doc comment on AgentPreset says "Spawn policy preset for sub-agents" — clear enough if you find and read it. But:
- The comment lives on the struct, not on the surrounding
AgentsConfig.
- The field
AgentsConfig.default_preset at crates/config/src/schema.rs L254 has only a one-line doc: "Optional default preset name used when spawn_agent.preset is omitted." The word "default" still dominates the field name.
- The config template at
crates/config/src/template.rs L210-L214 has one easy-to-miss inline comment: default_preset = "research" # Optional: used when spawn_agent.preset is omitted. Reading a 1000-line template, that comment disappears.
Validation is silent: crates/config/src/validate.rs L1202-L1210 validates that default_preset references an existing preset key, but doesn't warn when [agents.presets.*] has a populated tools.deny while [tools.policy] is empty — the exact signature of "user thinks they're configuring main-session policy via presets."
Expected behavior
Any combination of:
-
Docs fix (smallest) — explicit callout in the config template that [agents.presets.*] applies to sub-agents only, and that main-session tool policy goes under [tools.policy]. Near the [agents] section introduction, a sentence like:
NOTE: [agents.presets.*] configures sub-agents spawned via the spawn_agent tool only. For main-session tool allow/deny, use the top-level [tools.policy] section instead.
-
Validation warning — emit a diagnostic when [agents.presets.*] has a non-empty tools.deny AND [tools.policy] is empty, saying "you may have intended main-session tool policy; see [tools.policy]." crates/config/src/validate.rs already has the infrastructure for emitting warnings at L1202-L1210 (the existing default_preset check), so adding another diagnostic is mechanical.
-
Rename (breaking) — rename default_preset to default_subagent_preset or default_spawn_preset to remove the ambiguity. Big breaking change, probably not worth the churn, but would eliminate the trap permanently.
I'd suggest filing this as (1) + (2). Both are small and protective.
Steps to reproduce
Fresh moltis.toml:
[tools]
# ... general tools config ...
[agents]
default_preset = "full"
[agents.presets.full]
tools.deny = ["browser", "web_fetch", "firecrawl_scrape"]
sessions.can_send = true
Note the absence of [tools.policy].
Start the agent and inspect the tool schema list the main session sees. Via the web UI or the debug endpoint:
browser, web_fetch, firecrawl_scrape will all be present and callable.
- Users configuring this in good faith will assume the deny list worked and will only discover the gap when they find the LLM actually using a "denied" tool in a trace log.
Why this matters
This is a subtle trap that bites users who are actively trying to harden their MOLTIS deployment. The ones who don't configure tool policy at all are unaffected. But anyone coming in with a security hardening mindset will instinctively reach for the preset system (presets are a more sophisticated-looking control surface than a flat list) and will end up with zero enforcement on the surface that matters most.
Silent misconfiguration on security controls is the worst failure mode — user believes they're protected, gets zero protection, and has no signal that anything is wrong until something goes badly.
Is this a regression?
No, this never worked the way users expect
Moltis version
20260410.01
Install method
Built from source
Component
Configuration
Operating system
Other Linux (Alpine-based Fly.io container)
Additional context
Part of the same review pass as #631, #632, #633, #638 (closed), #639 (closed), #640, #641, #654, #655. We hit this in production — our generator was emitting a deny list under [agents.presets.full] tools.deny expecting it to affect our main autonomous agent, and we found zero enforcement after ~2 weeks. The fix for us was to also emit a top-level [tools.policy] section. Filing this so other users don't repeat our mistake.
What happened?
The
[agents.presets.*]config section withtools.allow/tools.denylooks like it should configure the main agent session's tool policy. The[agents] default_preset = "..."field reinforces that impression — "default" suggests "applies to the default agent, i.e. the main session." Users configuring a deny list under[agents.presets.full] tools.deny = ["browser", "web_fetch", ...]and settingdefault_preset = "full"naturally expect those tools to be hidden from the main agent.They are not. The preset system applies ONLY to sub-agents spawned via the
spawn_agenttool. The main session's tool filtering reads flat[tools.policy], which is a completely separate config section. An empty or absent[tools.policy]means the main session gets no tool filtering at all, even when[agents.presets.full] tools.denyis populated.This is not a code bug — the preset system works as implemented. But the naming, the template documentation, and the absence of any validation warning combine to create a trap where users configure tool hiding and silently get zero enforcement on the session that matters most.
We fell into this trap and ran for 2 weeks with a deny list that was silently no-op for the main agent. After discovering the gap, we had to add a top-level
[tools.policy]section to our generator.Evidence
Only consumer of
default_preset:crates/tools/src/spawn_agent.rsL193:The preset selection code lives entirely inside the
spawn_agenttool. Grep fordefault_presetacross the codebase confirms: the only non-test, non-schema consumers arespawn_agent.rsL193, L667, L671 — all inside the spawn_agent tool implementation.Main session uses flat
[tools.policy]:crates/chat/src/lib.rsL1431-L1443,effective_tool_policy:This reads
config.tools.policy.*exclusively. It never consultsconfig.agents.presets.*.apply_runtime_tool_filtersat L1445 calls it per turn for the main session.AgentPreset doc comment is accurate but buried:
crates/config/src/schema.rsL345-L378. The doc comment onAgentPresetsays "Spawn policy preset for sub-agents" — clear enough if you find and read it. But:AgentsConfig.AgentsConfig.default_presetatcrates/config/src/schema.rsL254 has only a one-line doc: "Optional default preset name used whenspawn_agent.presetis omitted." The word "default" still dominates the field name.crates/config/src/template.rsL210-L214 has one easy-to-miss inline comment:default_preset = "research" # Optional: used when spawn_agent.preset is omitted. Reading a 1000-line template, that comment disappears.Validation is silent:
crates/config/src/validate.rsL1202-L1210 validates thatdefault_presetreferences an existing preset key, but doesn't warn when[agents.presets.*]has a populatedtools.denywhile[tools.policy]is empty — the exact signature of "user thinks they're configuring main-session policy via presets."Expected behavior
Any combination of:
Docs fix (smallest) — explicit callout in the config template that
[agents.presets.*]applies to sub-agents only, and that main-session tool policy goes under[tools.policy]. Near the[agents]section introduction, a sentence like:Validation warning — emit a diagnostic when
[agents.presets.*]has a non-emptytools.denyAND[tools.policy]is empty, saying "you may have intended main-session tool policy; see[tools.policy]."crates/config/src/validate.rsalready has the infrastructure for emitting warnings at L1202-L1210 (the existingdefault_presetcheck), so adding another diagnostic is mechanical.Rename (breaking) — rename
default_presettodefault_subagent_presetordefault_spawn_presetto remove the ambiguity. Big breaking change, probably not worth the churn, but would eliminate the trap permanently.I'd suggest filing this as (1) + (2). Both are small and protective.
Steps to reproduce
Fresh
moltis.toml:Note the absence of
[tools.policy].Start the agent and inspect the tool schema list the main session sees. Via the web UI or the debug endpoint:
browser,web_fetch,firecrawl_scrapewill all be present and callable.Why this matters
This is a subtle trap that bites users who are actively trying to harden their MOLTIS deployment. The ones who don't configure tool policy at all are unaffected. But anyone coming in with a security hardening mindset will instinctively reach for the preset system (presets are a more sophisticated-looking control surface than a flat list) and will end up with zero enforcement on the surface that matters most.
Silent misconfiguration on security controls is the worst failure mode — user believes they're protected, gets zero protection, and has no signal that anything is wrong until something goes badly.
Is this a regression?
No, this never worked the way users expect
Moltis version
20260410.01
Install method
Built from source
Component
Configuration
Operating system
Other Linux (Alpine-based Fly.io container)
Additional context
Part of the same review pass as #631, #632, #633, #638 (closed), #639 (closed), #640, #641, #654, #655. We hit this in production — our generator was emitting a deny list under
[agents.presets.full] tools.denyexpecting it to affect our main autonomous agent, and we found zero enforcement after ~2 weeks. The fix for us was to also emit a top-level[tools.policy]section. Filing this so other users don't repeat our mistake.