Skip to content

feat: allow disabling/filtering Claude SDK built-in tools on ai.action #2655

Description

@eviltik

Update — scope clarification (post-investigation)

Same as for #2654 (cross-link): exposing allowed_tools on the ai.action form requires extending AgentActionArgs in packages/tracecat-ee/tracecat_ee/agent/schemas.py, which is in the EE module. The custom-source-level part is fully OSS.

We don't intend to ship code into the EE module ourselves. The OSS PR will include a defensive getattr so the cascade is forward-compatible — when EE exposes the field, the action-level override starts taking effect automatically with no further OSS change. Maintainers can apply the EE one-liner at merge time, or guide us towards a different layout before the PR opens.


TL;DR

ai.action always inlines the JSON schemas of the full Claude Code built-in toolset (Bash, Read, Write, Edit, Grep, Glob, MultiEdit, etc.) in the system prompt — even though the action is documented as a "simple LLM call, no tools" and runs with max_tool_calls=0.

That's ~3 940 input tokens of pure overhead per call (96 % of the system context), measured below.

This issue proposes exposing one optional field (allowed_tools: list[str] | None) at both the custom-source and action levels, mapping directly onto ClaudeAgentOptions.tools which already supports the full range (None = SDK default, [] = disable all built-ins, ["Bash","Read"] = whitelist).

Default None everywhere → zero behaviour change for existing workflows.

Measured impact

Tested directly against the bundled CLI 2.1.85 inside sysdio2_tracecat_agent_executor, ANTHROPIC_BASE_URL pointed at a remote Ollama running qwen3.5:9b. User prompt: "Reply: hi" (3 tokens). The CLI's --output-format json returns a usage.input_tokens field that captures system prompt + tool definitions + user prompt cumulatively.

# Configuration input_tokens Delta vs A
A CLI defaults (no flags) 4 096 reference
B --system-prompt "You are Qwen, by Alibaba." 4 096 0
C --system-prompt "..." --tools "" 158 −96 %
D --bare --system-prompt "..." --tools "" 157 −96 %

So roughly 96 % of the per-call overhead comes from the inlined JSON tool definitions. For an ai.action that's just doing a one-shot LLM completion (classification, extraction, summarization), this is pure waste — and on top of that, smaller models often hallucinate around tools they "see" but cannot actually call.

Capability already in the SDK

ClaudeAgentOptions.tools natively supports:

  • tools=None → no --tools flag → CLI applies its default (full toolset).
  • tools=[]--tools "" → all built-ins disabled.
  • tools=["Bash","Read"]--tools "Bash,Read" → whitelist.

The proposal is to surface this in the Tracecat schema.

Proposed change

Custom source (default)

Field Type Default
allowed_tools list[str] | None None

ai.action schema (per-call override)

Same field. When set, takes precedence over the source-level value.

Cascade rule

if action.allowed_tools is not None:
    resolved = action.allowed_tools          # action overrides source (even if [])
elif source.allowed_tools is not None:
    resolved = source.allowed_tools          # source default if no action value
else:
    resolved = None                           # SDK default (full toolset)

The runtime maps resolved directly onto ClaudeAgentOptions.tools.

Backward compatibility

  • New field defaults to Nonezero behaviour change for existing workflows or sources.
  • disallowed_tools and existing built-in filtering logic in Tracecat are untouched.
  • MCP servers (tracecat-registry) are not affected by this PR.

Note on pydantic_ai runtime

The pydantic-ai harness doesn't have the same notion of "implicit built-in tools" — it only sees the tools you give it. So allowed_tools would be a no-op there. We'd log a WARNING if it's set on a workflow that resolves to the pydantic-ai runtime, and document this clearly.

Scope of implementation

  • tracecat/db/models.py: allowed_tools JSONB NULL on agent_custom_provider.
  • Alembic migration.
  • tracecat/agent/provider/schemas.py: extend Create/Update/Read.
  • Registry action schema for ai.action: add allowed_tools.
  • tracecat/dsl/action.py:build_agent_args_activity: cascade.
  • tracecat/agent/runtime/claude_code/runtime.py: pass tools=resolved to ClaudeAgentOptions.
  • tracecat/agent/runtime/pydantic_ai/runtime.py: warn if set, ignore.
  • Frontend: types regenerated via just gen-client-ci. Custom source modal: add the field under "Agent settings". Action form picks it up automatically through schema generation.
  • Tests:
    • Unit: verify the cmd array produced by subprocess_cli._build_command includes --tools "" when tools=[], --tools "Bash,Read" when whitelist, and no --tools flag when None.
    • Cascade: matrix of source × action × None.
    • Token-overhead regression: integration test asserting input_tokens for tools=[] is well below the default.
  • Docs: update docs/agents/ai-action.mdx with a "Reducing token overhead" section.

Open question for maintainers

Two design alternatives we considered but rejected:

  1. Auto-disable tools when model_provider == "custom-model-provider". Pro: zero UX. Con: silent behaviour change for existing custom-source users — some might rely on tools being there.
  2. Auto-disable tools when max_tool_calls == 0. Same trade-off, with the bonus of fitting the ai.action semantics. Still a silent behaviour change though.

The opt-in design proposed here is more conservative and matches the spirit of Passthrough mode (you choose when to skip default behaviour). Happy to discuss if maintainers prefer one of the auto-disable variants.

Happy to send the PR. Splitting source-level and action-level fields into two PRs is doable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions