Skip to content

HERMES_TOOLS_SUBSET env var — worker-side tool gating (bridge to #210 R1/R2) #74

@PowerCreek

Description

@PowerCreek

Discussion: https://gist.github.com/PowerCreek/833cda14a6528f031fcc334305e56c63

Problem

After landing G1–G5 + #67 trio + devagentic#218 silo-v2 + #71 lazy-load, the poly-explorer worker spawned successfully but empties across all prompts that exercise MCP tools, even with #71's reduced preamble. Direct API probe with a single tool attached returns a perfect tool_call; hermes attaching all 33 tools per turn produces finish_reason=stop tool_calls=0 content="". Pure tool-paralysis from schema-attention overload.

This is the integration ceiling: the fused stack is architecturally complete but the per-call tool surface is too broad for current models to use without choking.

Root cause (static analysis)

  • agent.tools populated once at session boot via agent/agent_init.py:818:
    agent.tools = _ra().get_tool_definitions(enabled_toolsets=…, disabled_toolsets=…, …)
  • Same agent.tools attached to EVERY chat-completions request — three call sites at agent/conversation_loop.py:513 / 559 / 3471: tools=agent.tools or None. No per-turn filtering.
  • Existing --enable-toolset / --disable-toolset flags only work at TOOLSET (plugin) granularity. Post-G1-G5 stack pulls 33 tools across 6 plugins; toolset disabling is too coarse for surgical narrowing (disabling canvas loses all canvas tools as a unit).

Proposed fix

Add HERMES_TOOLS_SUBSET env var — comma-separated allow-list of tool names. When set, agent.tools is filtered to only those tools at session boot. Composes with existing toolset flags (subset narrows what's already enabled; doesn't override).

Implementation (~15 LOC)

Insert immediately after agent.tools = _ra().get_tool_definitions(…) in agent_init.py:818:

_subset_raw = (os.environ.get("HERMES_TOOLS_SUBSET") or "").strip()
if _subset_raw and agent.tools:
    _wanted = {n.strip() for n in _subset_raw.split(",") if n.strip()}
    if _wanted:
        _before = len(agent.tools)
        agent.tools = [
            t for t in agent.tools
            if (t.get("function") or {}).get("name") in _wanted
        ]
        if not agent.quiet_mode:
            _kept = sorted({(t.get("function") or {}).get("name", "?")
                            for t in agent.tools})
            print(f"🎯 HERMES_TOOLS_SUBSET narrowed tool surface: "
                  f"{_before}{len(agent.tools)} ({', '.join(_kept)})")

agent.valid_tool_names recomputation immediately below (agent.valid_tool_names = {tool["function"]["name"] for tool in agent.tools}) automatically reflects the filtered set — no extra change needed.

Operator usage

# Polynomial-explorer in observation mode (read-only):
HERMES_TOOLS_SUBSET=grafted_context_fetch,lane_h_list,lane_h_fetch,doc_search,silo_query hermes

# Polynomial-explorer in execution mode (read + confer):
HERMES_TOOLS_SUBSET=grafted_context_fetch,doc_search,doc_write,silo_query,confer_run hermes

# Default profile dev work (no narrowing needed):
hermes  # all 33 tools attached as today

Acceptance

  • HERMES_TOOLS_SUBSET=A,B,C env in the hermes process narrows agent.tools to just A/B/C (when those names exist in the underlying registry).
  • Empty / unset env preserves current behavior (no filtering).
  • Names not present in the registry are silently ignored (plugins add/remove tools at runtime; pre-validation would over-warn).
  • One INFO line at session start showing the narrowing effect (33 → 5 (grafted_context_fetch, …)).
  • 5-6 unit tests cover: env unset → no change · env set with subset → filtered · env set with names not in registry → silent skip · env set to empty string → no change · composability with enabled_toolsets (toolset filter runs first, env subset narrows further).
  • README addition in agent/ or docs/ documenting the env var + when to use it.

Why now

Out of scope

Severity

Quick unblock for polynomial-explorer + any future >5-tool vertical session. Small surgical change. Filing per orchestrator's direct ask.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions