Skip to content

[Bug]: [agents.presets.*] tool policies silently don't apply to the main agent session #656

@dmitriikeler

Description

@dmitriikeler

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:

  1. 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.

  2. 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.

  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions