Search by

berenger / ai-tool-policy-bundle

berenger

Symfony bundle providing a policy layer over the Symfony AI toolbox: tool filtering by tags, approval gating, and a debug command.

Package info

github.com/Berenger/ai-tool-policy-bundle

Type:symfony-bundle

pkg:composer/berenger/ai-tool-policy-bundle

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

0.15.3 2026-10-24 15:53 UTC

This package is auto-updated.

Last update: 2026-10-24 15:55:03 UTC


README

Latest version

Policy layer on top of the Symfony AI toolbox.

Filters which tools the model can see, and can ask for confirmation before running them.

Requirements

  • PHP >= 8.4
  • Symfony 7.4 or 8
  • symfony/ai-agent ^0.15

Installation

composer require berenger/ai-tool-policy-bundle

Versions

The bundle follows the symfony/ai minor it supports: the minor number is the same on both sides, so the installed pair is readable at a glance.

Bundle symfony/ai-agent
0.15.x ^0.15

The 0.10 line was withdrawn: that release predates the fix that actually wires the filter, so it exposed every tool, including the ones marked enabled: false. It is not installable anymore.

A release only happens when there is something to ship. A patch fixes the bundle; the minor changes when symfony/ai breaks and this adapter follows.

When a new symfony/ai minor is published: widen the constraint, run the test suite and the sandbox project against it, then tag <minor>.0.

Configuration

# config/packages/tool_policy.yaml
tool_policy:
    toolboxes:                            # ai.toolbox.<agent name>, the toolboxes to decorate
        - 'ai.toolbox.assistant'
    allowed_tags: []                      # empty = no tag filtering
    auto: false                           # also decorate the ai.toolbox services not listed

symfony/ai-bundle creates one toolbox service per agent, named ai.toolbox.<agent name> (here the agent declared under ai.agent.assistant). There is no ai.toolbox service: that name is only a tag. A toolbox that is not listed in toolboxes is not filtered: its agent sees every tool, including the ones marked enabled: false, unless auto is enabled.

With auto: true, every service tagged ai.toolbox that toolboxes does not list is decorated too, and toolboxes may then be empty. An agent added later is therefore filtered without touching this configuration, which is the point: check debug:llm-tools when you add one.

The filter decorates each toolbox with the default priority (0). A decorator of your own on the same toolbox with a negative priority wraps the filter: tools it adds are not filtered. Use a positive priority to stay under the filter.

Example with two agents and a filter:

# config/packages/ai.yaml
ai:
    agent:
        assistant:
            platform: 'ai.platform.openai'
            model: 'gpt-4o-mini'
            tools: true
        support:
            platform: 'ai.platform.openai'
            model: 'gpt-4o-mini'
            tools: true

# config/packages/tool_policy.yaml
tool_policy:
    toolboxes:
        - 'ai.toolbox.assistant'
        - 'ai.toolbox.support'
    allowed_tags: ['filesystem', 'http']

The same policy (allowed_tags) applies to every listed toolbox. Listing the same toolbox twice, or configuring no toolbox at all without auto, fails at container compilation.

allowed_tags is not merged across configuration files: a later declaration (for example under when@prod) replaces the previous list instead of adding to it.

toolbox_inner: 'ai.toolbox.assistant' (single toolbox) is still accepted for backward compatibility. Setting both toolboxes and toolbox_inner fails at container compilation: use toolboxes.

#[ToolBridge] attribute

Complements #[AsTool]. Name and description stay handled by Symfony AI.

use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
use ToolPolicy\Bundle\Attribute\ToolBridge;

#[AsTool(name: 'read_file', description: 'Read a file')]
#[ToolBridge(enabled: true, tags: ['filesystem'], requiresApproval: true)]
final class ReadFileTool
{
    // ...
}
Option Default Role
enabled true Hides the tool when false
tags [] Tags used by the allowed_tags filter
requiresApproval false Asks for confirmation before execution
name null Policy key, defaults to the #[AsTool] name (see below)

The policy is read from the PHP attribute; the tool_policy.bridge tag only serves to collect the service. Tagging a service by hand with tool_policy.bridge and attributes (requiresApproval, ...) has no effect.

Without #[ToolBridge], the tool stays visible as-is when allowed_tags is empty, and is hidden as soon as allowed_tags is set.

A hidden tool is not executable either: calling it by name returns Tool "<name>" is not available. instead of running it.

#[AsTool] is repeatable. A single #[ToolBridge] applies to every #[AsTool] of the class; otherwise declare exactly one #[ToolBridge] per #[AsTool], paired by declaration order. Any other combination, or a #[ToolBridge] on a class without #[AsTool], fails at container compilation.

Renamed tools

An application can rename a tool in its ai: configuration. The toolbox then exposes it under the new name, and the policy keyed on the #[AsTool] name no longer matches: the tool is treated as if it had no #[ToolBridge] (no filter, no approval). Align the #[ToolBridge] name with the exposed name:

# config/packages/ai.yaml
ai:
    agent:
        assistant:
            tools:
                services:
                    - service: 'App\Tool\ReadFileTool'
                      name: 'read_file_v2'          # name and description are both
                      description: 'Read a file'    # required for the rename to apply
#[AsTool(name: 'read_file', description: 'Read a file')]
#[ToolBridge(name: 'read_file_v2', tags: ['filesystem'], requiresApproval: true)]
final class ReadFileTool
{
    // ...
}

The policy is then registered under both read_file and read_file_v2: the rename is per agent, so another agent may still expose the tool as read_file. A policy only applies to a tool of the class that declares it: a name equal to the name of a tool from another class does not hand the policy over to it (that tool is treated as having no #[ToolBridge]). debug:llm-tools fails on a name that matches no tool of its class.

An empty name fails at container compilation in the same way, and so does a name already used by another #[ToolBridge] tool. A single #[ToolBridge] shared by several #[AsTool] cannot set name.

Approval

If requiresApproval: true, the configured ApprovalGateInterface service is asked before running the tool.

The bundle ships a DenyAllGate and aliases the interface to it, so an application always boots: a gated tool is then simply denied (Tool "<name>" execution was not approved.) instead of the container failing to compile. Provide your own implementation, for example an interactive CLI prompt, to decide case by case.

// config/services.yaml
ToolPolicy\Bundle\Approval\ApprovalGateInterface:
    alias: App\Agent\MyApprovalGate

Debug

php bin/console debug:llm-tools
php bin/console debug:llm-tools --exposed
php bin/console debug:llm-tools --tag=filesystem

Shows, for each filtered toolbox, enabled, tags, approval, and whether the tool is exposed to the model, then the #[ToolBridge] tools exposed by no toolbox. --tag and --exposed apply to every section. Exits with an error when a #[ToolBridge(name:)] alias matches no tool of its class in any toolbox.

The decorators are registered as tool_policy.filtered_toolbox, tool_policy.filtered_toolbox.1, ... (one per entry of toolboxes, in order, then one per toolbox decorated by auto); ToolPolicy\Bundle\Toolbox\FilteredToolbox is an alias to the first one.