This is an automated email from the ASF dual-hosted git repository.

kaxil pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 72d612c357d Add restricted-agent examples to common.ai toolset guides 
(#74379)
72d612c357d is described below

commit 72d612c357dfeb73108713cba6f2afc4d07a18d6
Author: Kaxil Naik <[email protected]>
AuthorDate: Wed Oct 7 06:18:17 2026 +0100

    Add restricted-agent examples to common.ai toolset guides (#74379)
---
 providers/common/ai/docs/toolsets/datafusion.rst   | 50 +++++++++++
 providers/common/ai/docs/toolsets/hook.rst         | 57 ++++++++++++-
 .../common/ai/docs/toolsets/object_storage.rst     | 38 +++++++++
 providers/common/ai/docs/toolsets/skills.rst       | 38 +++++++++
 providers/common/ai/docs/toolsets/sql.rst          | 97 ++++++++++++++++++++++
 .../common/ai/example_dags/example_agent_skills.py | 29 +++++++
 .../ai/example_dags/example_datafusion_toolset.py  | 72 ++++++++++++++++
 .../common/ai/example_dags/example_hook_toolset.py | 57 +++++++++++++
 .../example_dags/example_object_storage_toolset.py | 35 ++++++++
 .../common/ai/example_dags/example_sql_toolset.py  | 65 +++++++++++++++
 10 files changed, 536 insertions(+), 2 deletions(-)

diff --git a/providers/common/ai/docs/toolsets/datafusion.rst 
b/providers/common/ai/docs/toolsets/datafusion.rst
index 5c494caae74..a1aa337b38c 100644
--- a/providers/common/ai/docs/toolsets/datafusion.rst
+++ b/providers/common/ai/docs/toolsets/datafusion.rst
@@ -75,6 +75,56 @@ The ``DataFusionEngine`` is created lazily on the first tool 
call. This
 toolset requires the ``datafusion`` extra of
 ``apache-airflow-providers-common-sql``.
 
+.. _datafusion-toolset-restricted:
+
+Restricting the agent
+---------------------
+
+With ``allow_writes=False`` (the default), the tables you register are the 
only ones
+the agent can query; there is no separate allow-list. This agent can query two 
tables,
+cannot write, and gets bounded results:
+
+.. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_datafusion_toolset.py
+    :language: python
+    :start-after: [START howto_toolset_datafusion_restricted]
+    :end-before: [END howto_toolset_datafusion_restricted]
+
+Run against an S3 endpoint where an ``acme-payroll`` bucket sits beside the 
reports,
+these queries were refused, and the model got the error back to correct:
+
+``SELECT * FROM payroll``
+    ``error: Error while executing query: DataFusion error: 
Diagnostic(Diagnostic {
+    kind: Error, message: "table 'payroll' not found", ...``
+
+``SELECT * FROM 's3://acme-payroll/salaries.csv'``
+    ``error: Error while executing query: DataFusion error: 
Diagnostic(Diagnostic {
+    kind: Error, message: "table 's3://acme-payroll/salaries.csv' not found", 
...``
+
+``CREATE TABLE copy AS SELECT * FROM sales``
+    ``error: Statement type 'Create' is not allowed. Allowed types: Select, 
Union,
+    Intersect, Except. Only read-only SELECT-family queries are allowed unless
+    allow_writes is enabled; check the SQL syntax and statement type, then try
+    again.``
+
+The second query shows that a URL in the SQL is looked up as a table name, not 
read
+as a file. A query over the registered tables runs:
+
+.. code-block:: sql
+
+    SELECT s.region, CAST(sum(r.amount) AS DOUBLE) / sum(s.amount) AS 
return_rate
+    FROM sales s JOIN returns r ON r.region = s.region
+    GROUP BY s.region ORDER BY return_rate DESC
+
+It returns:
+
+.. code-block:: json
+
+    
{"columns":["region","return_rate"],"rows":[["AMER",0.5],["EMEA",0.2]],"row_count":2}
+
+The object store is created for the whole bucket, not the prefix each
+``DataSourceConfig`` registers, so give ``aws_reports_reader`` credentials 
that can
+read only those prefixes.
+
 Parameters
 ----------
 
diff --git a/providers/common/ai/docs/toolsets/hook.rst 
b/providers/common/ai/docs/toolsets/hook.rst
index 15a6ad0d2b8..84d93871000 100644
--- a/providers/common/ai/docs/toolsets/hook.rst
+++ b/providers/common/ai/docs/toolsets/hook.rst
@@ -107,8 +107,11 @@ the Dag author's decision, such as which bucket a storage 
hook reads, pin them:
     )
 
 A pinned argument is left out of the schema the model sees and passed to every 
allowed
-method. If the model supplies it anyway, the call is refused and the model is 
told the
-argument is fixed.
+method. If the model supplies it anyway, the call is refused. For a method 
with named
+parameters, such as ``S3Hook.read_key``, argument validation refuses it as an 
extra
+input (see :ref:`hook-toolset-restricted`). A method that names the parameter 
and also
+takes ``**kwargs`` would accept it, so the toolset refuses it itself and tells 
the
+model the argument is fixed.
 
 A pin binds one parameter name, so every allowed method has to take it by that 
name.
 When one does not, the toolset raises ``ValueError`` when it is created: a 
method that
@@ -123,6 +126,56 @@ Pinned values are passed as written: they are not rendered 
as templates, and the
 part of what ``AgentOperator(durable=True)`` fingerprints, so change one only 
between Dag
 runs, not between the tries of one.
 
+.. _hook-toolset-restricted:
+
+Restricting the agent
+---------------------
+
+``allowed_methods`` decides which hook methods become tools, and 
``pinned_arguments``
+decides which of their arguments the model cannot set. This agent can list and 
read
+one bucket through ``S3Hook``, and nothing else:
+
+.. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_hook_toolset.py
+    :language: python
+    :start-after: [START howto_toolset_hook_restricted]
+    :end-before: [END howto_toolset_hook_restricted]
+
+The model is offered two tools: ``s3_read_key``, which takes only ``key``, and
+``s3_list_keys``, whose parameters include ``prefix`` but not ``bucket_name``. 
Run
+against an S3 endpoint that also holds an ``acme-payroll`` bucket, a call that 
names
+that bucket was refused before it reached S3, with this message to the model:
+
+.. code-block:: text
+
+    1 validation error:
+    ```json
+    [
+      {
+        "type": "extra_forbidden",
+        "loc": [
+          "bucket_name"
+        ],
+        "msg": "Extra inputs are not permitted",
+        "input": "acme-payroll"
+      }
+    ]
+    ```
+
+    Fix the errors and try again.
+
+A call to a method that is not listed, such as ``s3_delete_objects``, got
+``Unknown tool name: 's3_delete_objects'. Available tools: 's3_list_keys',
+'s3_read_key'``. The model can correct both kinds of call and carry on.
+
+An exception from the hook is different: it fails the run, and the task with 
it.
+Reading a key that does not exist ended the run with ``ClientError: An error 
occurred
+(404) when calling the HeadObject operation: Not Found``.
+
+The pin fixes the bucket and leaves every key in it to the model. Give
+``aws_reports_reader`` credentials that can read only that bucket, so the 
connection
+holds the same limit if a method you expose later reaches another bucket some 
other
+way.
+
 Parameters
 ----------
 
diff --git a/providers/common/ai/docs/toolsets/object_storage.rst 
b/providers/common/ai/docs/toolsets/object_storage.rst
index 67984d575e7..ed3d6577724 100644
--- a/providers/common/ai/docs/toolsets/object_storage.rst
+++ b/providers/common/ai/docs/toolsets/object_storage.rst
@@ -72,6 +72,44 @@ the task: an image or PDF, a binary file, a file larger than 
``max_read_bytes``
 by default), a corrupt file, a path that does not exist, or one the connection 
may not
 read.
 
+.. _object-storage-toolset-restricted:
+
+Restricting the agent
+---------------------
+
+This agent can read only under ``s3://acme-reports/finance/``, and every list 
and read
+is bounded:
+
+.. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_object_storage_toolset.py
+    :language: python
+    :start-after: [START howto_toolset_object_storage_restricted]
+    :end-before: [END howto_toolset_object_storage_restricted]
+
+Run against an S3 endpoint where an ``acme-payroll`` bucket sits beside the 
reports,
+these reads were refused. Each refusal comes back to the model as the tool's 
result,
+so it does not use ``max_retries``, and the run carries on:
+
+``../../acme-payroll/salaries.csv``
+    ``'../../acme-payroll/salaries.csv' leaves the storage root; '..' is not 
allowed.``
+
+``s3://acme-payroll/salaries.csv``
+    ``'s3://acme-payroll/salaries.csv' is not a relative path. Name files 
relative to
+    the storage root.``
+
+``/etc/passwd``
+    ``'/etc/passwd' is not a relative path. Name files relative to the storage 
root.``
+
+``2026-09/chart.png``
+    ``'2026-09/chart.png' is a png file, which this tool cannot read as text.``
+
+``2026-09/ledger.csv``, a 1.5 MB file
+    ``'2026-09/ledger.csv' is larger than the 1.0MB this tool reads.``
+
+Because refused reads cost nothing from ``max_retries``, bound a run that keeps
+asking with the operator's ``usage_limits``. The path check is the toolset's 
only
+boundary on location; credentials that can read only 
``s3://acme-reports/finance/`` keep that limit
+if the check has a gap.
+
 Parameters
 ----------
 
diff --git a/providers/common/ai/docs/toolsets/skills.rst 
b/providers/common/ai/docs/toolsets/skills.rst
index ed32a2fb2c7..bb1cdcc6bf4 100644
--- a/providers/common/ai/docs/toolsets/skills.rst
+++ b/providers/common/ai/docs/toolsets/skills.rst
@@ -82,6 +82,44 @@ need strict isolation.
     ``branch`` to a trusted ref, and treat skill contents as code that runs in
     your environment.
 
+.. _agent-skills-restricted:
+
+Restricting the agent
+---------------------
+
+This agent can read skills but cannot run any script a skill ships, and files 
that
+match the ``exclude_resources`` patterns stay out of its reach. The example 
skills ship
+no scripts, so the exclusion matters once a skill adds one:
+
+.. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_agent_skills.py
+    :language: python
+    :start-after: [START howto_operator_agent_skills_restricted]
+    :end-before: [END howto_operator_agent_skills_restricted]
+
+The model is offered ``list_skills``, ``load_skill`` and 
``read_skill_resource``. A
+call to the excluded tool got ``Unknown tool name: 'run_skill_script'. 
Available
+tools: 'list_skills', 'load_skill', 'read_skill_resource'``.
+
+The example skills contain no file the patterns match, so the next run used a 
copy of
+them with ``warehouse.env``, ``secrets/token.txt`` and ``reference.md`` added 
to
+``sql-reporting``. ``load_skill`` listed only ``reference.md`` as a resource, 
and
+reading an excluded file got:
+
+.. code-block:: text
+
+    Resource 'warehouse.env' not found in skill 'sql-reporting'. Available 
resources: ['reference.md']. Use the exact name from load_skill output.
+
+The skills tools allow the model one correction each, and the agent's 
``retries``
+does not change that. A second refused read in a row failed the run with
+``UnexpectedModelBehavior``, even with ``retries`` set to ``{"tools": 3}``:
+
+.. code-block:: text
+
+    Tool 'read_skill_resource' exceeded max retries count of 1. Consider 
raising the retry limit, or see the docs on tool retries: 
https://pydantic.dev/docs/ai/tools-toolsets/tools-advanced/#tool-retries
+
+``exclude_resources`` hides files from the resource tools only. A skill's 
scripts can
+still read them, which is why the example excludes ``run_skill_script`` as 
well.
+
 Parameters
 ----------
 
diff --git a/providers/common/ai/docs/toolsets/sql.rst 
b/providers/common/ai/docs/toolsets/sql.rst
index e59a0e1a88a..6d0a85b6d71 100644
--- a/providers/common/ai/docs/toolsets/sql.rst
+++ b/providers/common/ai/docs/toolsets/sql.rst
@@ -65,6 +65,103 @@ rejects by scanning the parsed statement for write 
operations. When
 table, so its target must be on the list, while ``SHOW`` enumerates objects 
beyond
 any single table and is rejected outright (see 
:ref:`allowed-tables-enforcement`).
 
+.. _sql-toolset-restricted:
+
+Restricting the agent
+---------------------
+
+Every limit can be set on one task: the toolset holds most of them, and the
+operator holds the tool-call limit. This agent can query two tables, cannot 
write, gets
+bounded results, and has a budget for both refused and successful calls:
+
+.. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_sql_toolset.py
+    :language: python
+    :start-after: [START howto_toolset_sql_restricted]
+    :end-before: [END howto_toolset_sql_restricted]
+
+A refused query never reaches the database. The model gets the reason back as 
an
+error it can correct, and the run carries on. Run against a Postgres warehouse 
that
+also holds a ``secrets`` table, these queries were refused with these messages:
+
+``SELECT token FROM secrets``
+    ``The query tool failed: Query references tables that are not in the 
allowed
+    tables list: secrets. Use list_tables to see the allowed tables.``
+
+``SELECT pg_read_file('/etc/passwd')``
+    ``The query tool failed: Query uses a data source that cannot be checked 
against
+    allowed_tables: function(s) the parser cannot verify against allowed_tables
+    (pg_read_file); if these functions are trusted, permit them via
+    allowed_functions. Query the allowed tables directly: use list_tables to 
see
+    them.``
+
+``DELETE FROM orders``
+    ``The query tool failed: Statement type 'Delete' is not allowed. Allowed 
types:
+    Select, Union, Intersect, Except, Describe, Show``
+
+Each message ends with the same two lines:
+
+.. code-block:: text
+
+    Use the list_tables and get_schema tools to inspect the database, then fix 
the query and try again.
+
+    Fix the errors and try again.
+
+This query runs, because the example lists ``json_build_object`` in
+``allowed_functions``:
+
+.. code-block:: sql
+
+    SELECT c.region, json_build_object('revenue', sum(o.amount)) AS revenue
+    FROM orders o JOIN customers c ON c.id = o.customer_id
+    GROUP BY c.region ORDER BY c.region
+
+It returns:
+
+.. code-block:: json
+
+    
{"columns":["region","revenue"],"rows":[["AMER",{"revenue":50}],["EMEA",{"revenue":200}]],"row_count":2}
+
+Without ``allowed_functions``, the same query is refused with the same message 
as
+``pg_read_file``, naming ``json_build_object``.
+
+The two budgets count different calls, and running out of either fails the 
task.
+``max_retries`` counts refused and failed calls, and a fourth refused 
``query`` call
+in a row ends the run with ``UnexpectedModelBehavior``:
+
+.. code-block:: text
+
+    Tool 'query' exceeded max retries count of 3. Consider raising the retry 
limit, or see the docs on tool retries: 
https://pydantic.dev/docs/ai/tools-toolsets/tools-advanced/#tool-retries
+
+``tool_calls_limit`` counts successful calls only, and fails the run with
+``UsageLimitExceeded`` before any call that would take the count past 20:
+
+.. code-block:: text
+
+    The next tool call(s) would exceed the tool_calls_limit of 20 
(tool_calls=21). Consider raising the limit, or see the docs on usage limits 
for budget-aware patterns: 
https://pydantic.dev/docs/ai/core-concepts/agent/#usage-limits
+
+The task's own ``retries`` then decide whether it runs again, and the budgets 
differ
+there too. ``max_retries`` starts again on each attempt. On Airflow 3.3 and 
later,
+``usage_limits`` counts across every attempt, so a retry after 
``tool_calls_limit``
+ran out fails at its first tool call; see
+:ref:`the usage budget <agent-usage-budget>`. ``UsageLimits`` also keeps
+pydantic-ai's default ``request_limit`` of 50 model requests, which refused 
calls
+use up too.
+
+``allowed_tables`` works by parsing the SQL, so a query the parser reads 
differently
+from the database, or a function listed in ``allowed_functions``, can get past 
it.
+The connection's role is the limit that holds regardless. The example's
+``warehouse_agent_reader`` connection logs in as a role created with:
+
+.. code-block:: sql
+
+    CREATE ROLE warehouse_agent_reader LOGIN PASSWORD '...';
+    GRANT SELECT ON orders, customers TO warehouse_agent_reader;
+
+With that role and no ``allowed_tables``, ``SELECT token FROM secrets`` reaches
+Postgres, which refuses it, and the model gets ``The query tool failed: 
permission
+denied for table secrets``. :ref:`allowed-tables-enforcement` lists what the 
parser
+checks and where it stops.
+
 Multi-schema warehouses
 -------------------------
 
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent_skills.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent_skills.py
index d7523e608a0..f922b40993d 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent_skills.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent_skills.py
@@ -109,3 +109,32 @@ def example_agent_skills_git():
 # [END howto_operator_agent_skills_git]
 
 example_agent_skills_git()
+
+
+# ---------------------------------------------------------------------------
+# 3. Skills the model can read but not run
+# ---------------------------------------------------------------------------
+
+
+# [START howto_operator_agent_skills_restricted]
+@dag(tags=["example"])
+def example_agent_skills_restricted():
+    AgentOperator(
+        task_id="report_writer",
+        prompt="Review this query against our SQL reporting conventions: 
SELECT * FROM orders",
+        llm_conn_id="pydanticai_default",
+        toolsets=[
+            AgentSkillsToolset(
+                sources=[str(SKILLS_DIR)],
+                # Hide the tool that runs a skill's scripts on the worker.
+                exclude_tools={"run_skill_script"},
+                # Keep matching files out of the resources the model can list 
and read.
+                exclude_resources=["*.env", "secrets/*"],
+            )
+        ],
+    )
+
+
+# [END howto_operator_agent_skills_restricted]
+
+example_agent_skills_restricted()
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_datafusion_toolset.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_datafusion_toolset.py
new file mode 100644
index 00000000000..9e8670f108a
--- /dev/null
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_datafusion_toolset.py
@@ -0,0 +1,72 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Example Dag: an agent that runs read-only SQL over two registered tables of 
files on S3."""
+
+from __future__ import annotations
+
+from airflow.providers.common.ai.operators.agent import AgentOperator
+from airflow.providers.common.compat.sdk import dag
+
+try:
+    from airflow.providers.common.ai.toolsets.datafusion import 
DataFusionToolset
+    from airflow.providers.common.sql.config import DataSourceConfig
+except Exception:
+    DataFusionToolset = None  # type: ignore[assignment,misc]
+
+
+if DataFusionToolset is not None:
+    # [START howto_toolset_datafusion_restricted]
+    @dag(tags=["example"])
+    def example_datafusion_toolset_restricted():
+        AgentOperator(
+            task_id="returns_by_region",
+            prompt="Which region had the highest return rate in September?",
+            llm_conn_id="pydanticai_default",
+            toolsets=[
+                DataFusionToolset(
+                    # Register only what the agent may query: there is no 
other allow-list.
+                    datasource_configs=[
+                        DataSourceConfig(
+                            conn_id="aws_reports_reader",
+                            table_name="sales",
+                            uri="s3://acme-reports/finance/sales/",
+                            format="parquet",
+                        ),
+                        DataSourceConfig(
+                            conn_id="aws_reports_reader",
+                            table_name="returns",
+                            uri="s3://acme-reports/finance/returns/",
+                            format="csv",
+                        ),
+                    ],
+                    # The default: allow only SELECT-family statements.
+                    allow_writes=False,
+                    # Return at most 100 rows and 32 KiB from one query.
+                    max_rows=100,
+                    max_result_bytes=32 * 1024,
+                    # Summarize a table wider than 50 columns instead of 
listing every column.
+                    max_columns=50,
+                    # Let the model correct a refused query, or one naming an 
unknown table or
+                    # column, up to 3 times.
+                    max_retries=3,
+                )
+            ],
+        )
+
+    # [END howto_toolset_datafusion_restricted]
+
+    example_datafusion_toolset_restricted()
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_hook_toolset.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_hook_toolset.py
new file mode 100644
index 00000000000..88dc99aa108
--- /dev/null
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_hook_toolset.py
@@ -0,0 +1,57 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Example Dag: an agent that can list and read one S3 bucket through 
``S3Hook``, and nothing else."""
+
+from __future__ import annotations
+
+from airflow.providers.common.ai.operators.agent import AgentOperator
+from airflow.providers.common.ai.toolsets import HookToolset
+from airflow.providers.common.compat.sdk import dag
+
+try:
+    from airflow.providers.amazon.aws.hooks.s3 import S3Hook
+except ImportError:
+    S3Hook = None  # type: ignore[assignment,misc]
+
+
+if S3Hook is not None:
+    # [START howto_toolset_hook_restricted]
+    @dag(tags=["example"])
+    def example_hook_toolset_restricted():
+        AgentOperator(
+            task_id="read_reports",
+            prompt="Summarize the September finance report.",
+            llm_conn_id="pydanticai_default",
+            toolsets=[
+                HookToolset(
+                    # Log in with credentials scoped to the reports bucket.
+                    S3Hook(aws_conn_id="aws_reports_reader"),
+                    # Offer these two methods as tools; no other S3Hook method 
is reachable.
+                    allowed_methods=["list_keys", "read_key"],
+                    # Fix the bucket: the model never sees the argument and 
cannot set it.
+                    pinned_arguments={"bucket_name": "acme-reports"},
+                    # Name the tools s3_list_keys and s3_read_key.
+                    tool_name_prefix="s3_",
+                    # Let the model correct an invalid call up to 2 times.
+                    max_retries=2,
+                )
+            ],
+        )
+
+    # [END howto_toolset_hook_restricted]
+
+    example_hook_toolset_restricted()
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_object_storage_toolset.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_object_storage_toolset.py
index 3cd86eb9fb5..74a5000e784 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_object_storage_toolset.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_object_storage_toolset.py
@@ -18,6 +18,7 @@
 
 from __future__ import annotations
 
+from airflow.providers.common.ai.operators.agent import AgentOperator
 from airflow.providers.common.ai.toolsets import ObjectStorageToolset
 from airflow.providers.common.compat.sdk import dag, task
 
@@ -46,3 +47,37 @@ def example_object_storage_toolset():
 
 
 example_object_storage_toolset()
+
+
+# [START howto_toolset_object_storage_restricted]
+@dag(tags=["example"])
+def example_object_storage_toolset_restricted():
+    AgentOperator(
+        task_id="read_finance_reports",
+        prompt="Summarize the September finance report.",
+        llm_conn_id="pydanticai_default",
+        toolsets=[
+            ObjectStorageToolset(
+                # Resolve every path the model names under this root. The 
toolset never writes.
+                "s3://acme-reports/finance/",
+                # Credentials scoped to that prefix keep the limit if the path 
check has a gap.
+                conn_id="aws_reports_reader",
+                # List at most 50 entries per call; the model pages through 
the rest.
+                max_files=50,
+                # Refuse a file larger than 1 MiB, measured after 
decompression.
+                max_read_bytes=1024 * 1024,
+                # Return at most 16 KiB from one read; the model pages through 
a longer text file.
+                max_output_bytes=16 * 1024,
+                # Name the tools reports_list_files, reports_get_file_info and 
reports_read_file.
+                tool_prefix="reports",
+                # Let the model correct an invalid call up to 2 times.
+                max_retries=2,
+            )
+        ],
+    )
+
+
+# [END howto_toolset_object_storage_restricted]
+
+
+example_object_storage_toolset_restricted()
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_sql_toolset.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_sql_toolset.py
new file mode 100644
index 00000000000..33d4cc33b71
--- /dev/null
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_sql_toolset.py
@@ -0,0 +1,65 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Example Dag: a SQL agent held to two tables, read-only, with bounded 
results and a call budget."""
+
+from __future__ import annotations
+
+from pydantic_ai.usage import UsageLimits
+
+from airflow.providers.common.ai.operators.agent import AgentOperator
+from airflow.providers.common.compat.sdk import dag
+
+try:
+    from airflow.providers.common.ai.toolsets.sql import SQLToolset
+except Exception:
+    SQLToolset = None  # type: ignore[assignment,misc]
+
+
+if SQLToolset is not None:
+    # [START howto_toolset_sql_restricted]
+    @dag(tags=["example"])
+    def example_sql_toolset_restricted():
+        AgentOperator(
+            task_id="revenue_by_region",
+            prompt="What was last week's revenue by region?",
+            llm_conn_id="pydanticai_default",
+            toolsets=[
+                SQLToolset(
+                    # Log in as a role granted SELECT on orders and customers 
only.
+                    db_conn_id="warehouse_agent_reader",
+                    # Refuse any query that reaches another table or a source 
the parser cannot check.
+                    allowed_tables=["orders", "customers"],
+                    # Accept this function, which the SQL parser does not 
recognize.
+                    allowed_functions=["json_build_object"],
+                    # The default: allow only SELECT-family, DESCRIBE and SHOW 
statements.
+                    allow_writes=False,
+                    # Return at most 100 rows and 32 KiB from one query.
+                    max_rows=100,
+                    max_result_bytes=32 * 1024,
+                    # Summarize a table wider than 50 columns instead of 
listing every column.
+                    max_columns=50,
+                    # Let the model correct a refused or failed call up to 3 
times.
+                    max_retries=3,
+                )
+            ],
+            # Fail the run before a 21st successful tool call, counted across 
retries on Airflow 3.3+.
+            usage_limits=UsageLimits(tool_calls_limit=20),
+        )
+
+    # [END howto_toolset_sql_restricted]
+
+    example_sql_toolset_restricted()

Reply via email to