Skip to content

fix(agents): @tool silently discards unknown keyword arguments, so a typo'd timeout or an unsupported risk_tier registers with the default #2840

Description

@itomek

Problem

@tool accepts arbitrary keyword arguments and silently discards them
(src/gaia/agents/base/tools.py:19-25):

def tool(func=None, *, atomic=False, display_label=None, timeout=None,
         **kwargs) -> Callable:   # <- kwargs documented as "ignored, for
                                  #    backward compatibility", never read

So a misspelled or unsupported decorator argument produces no error, no warning,
and no log line — the tool registers with the silent default instead. Reproduced
against the real module:

@tool(risk_tier='destructive', atmoic=True, timeuot=600)
def demo_tool(x: str) -> str:
    'Demo.'
registry entry: {"name": "demo_tool", "description": "Demo.",
                 "parameters": {"x": {"type": "string", "required": true}},
                 "atomic": false, "display_label": null, "timeout": null}
risk_tier stored? False
atomic  (typed "atmoic")  -> False   # silently default
timeout (typed "timeuot") -> None    # silently default

This is a Fail-Loudly violation in shared base code: the caller asked for
behaviour, got the opposite, and nothing said so. Every agent in the repo goes
through this decorator.

Two concrete ways it bites today

1. A timeout typo silently un-caps a long tool. triage_inbox declares
@tool(timeout=600.0) (hub/agents/email/python/gaia_agent_email/tools/read_tools.py:2897)
precisely because it legitimately runs long. Mistype the kwarg and it falls back
to the global GAIA_AGENT_TOOL_TIMEOUT with no signal — surfacing later as an
unexplained mid-triage timeout, far from the cause.

2. The email spec instructs a parameter that does not exist.
docs/plans/email-triage-agent.mdx §8 writes @tool(risk_tier="read"),
@tool(risk_tier="write") and @tool(risk_tier="destructive") throughout, and
its own prerequisite note says the decorator must be extended with risk_tier
"before C1 ships". It never was — risk_tier appears nowhere in
src/gaia/agents/base/. Anyone implementing that spec literally gets a
destructive tool that registers cleanly and is gated by nothing, with no error to
tell them. (The security outcome is currently fine — the spec's named interim
gate, CONFIRMATION_REQUIRED_TOOLS, is real and in use at
hub/agents/email/python/gaia_agent_email/agent.py:628 — but that is the fallback
holding, not the decorator behaving.)

Found while auditing the email agent's tool surface (#2835/#2836); the bug is
core-framework, not email-specific.

Outcome

An unsupported keyword argument to @tool fails loudly at import time, naming the
offending argument and the tool, instead of registering a tool that silently
ignores it.

Acceptance criteria

  • An unknown kwarg raises TypeError at decoration (import) time, with the
    tool name and the accepted set in the message:
    TypeError: @tool(...) got unexpected keyword argument 'risk_tier' for tool 'demo_tool'. Accepted: atomic, display_label, timeout.
  • A near-miss of a real parameter behaves identically — atmoic=True and
    timeuot=600 each raise rather than silently defaulting.
  • Every existing @tool(...) call site in the repo still imports cleanly.
    Sweep first: the **kwargs sink means an unsupported kwarg may already
    be in use somewhere and passing silently. Grep every @tool( call with
    arguments before changing the signature, and list what was found in the PR
    — if a real call site relies on a discarded kwarg, that is its own bug to
    surface, not a reason to keep the sink.
  • Unit tests cover: accepted kwargs unchanged; unknown kwarg raises; typo of
    each accepted name raises; bare @tool and @tool() both still work.
  • The **kwargs parameter and its "ignored, for backward compatibility"
    docstring line are removed, not merely validated around.

Scope & expectations

  • Where: src/gaia/agents/base/tools.py (the tool decorator). Tests in
    tests/unit/.
  • Out of scope:
  • Constraints:
    • Fail at decoration time, not first call — a bad decorator argument is an
      import-time programming error and should never reach runtime.
    • Keep both @tool and @tool(...) call syntaxes working; the repo uses both.

How to verify

CLI surface — show the new failure and the preserved behaviour:

  1. A scratch module using @tool(risk_tier="destructive") now fails at import
    with the exact message above (paste it).
  2. python -m pytest tests/unit/ -q green — proves no existing call site relied
    on the silent sink.
  3. gaia chat (or any agent) starts and lists its tools normally, proving
    registration is otherwise unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingp2low priority

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions