Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion python/packages/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ agent_framework/
- **`SkillScript`** - An executable script attached to a skill; holds either an inline `function` (code-defined, runs in-process) or a `path` to a file on disk (file-based, delegated to a runner). Exactly one must be provided.
- **`SkillScriptRunner`** - Protocol for file-based script execution. Any callable matching `(skill, script, args) -> Any` satisfies it. Code-defined scripts do not use a runner.
- **`SkillScriptArgumentParser`** - Public type alias for an optional callable `(raw args: dict | list[str] | str | None) -> dict | None` that converts the raw `args` value before an `InlineSkillScript` runs (applied before the inline list-args guard). It is an opt-in customization hook (port of .NET PR #6498) that lets callers support backends sending tool-call arguments in a non-conforming shape (e.g. vLLM JSON strings). The output is constrained to a `dict` (named keyword arguments) or `None`, because inline scripts bind arguments by keyword name. Supply it via the `argument_parser=` constructor arg on `InlineSkillScript`, `InlineSkill` (default for scripts added via `@skill.script`), or `ClassSkill` (default for scripts discovered via `@ClassSkill.script`). When `None` (the default), the raw value is used unchanged. File-based scripts are unaffected (their runner owns arg handling).
- **`SkillsProvider`** - Context provider (extends `ContextProvider`) that discovers file-based skills from `SKILL.md` files and/or accepts code-defined `Skill` instances. Follows progressive disclosure: advertise → load → read resources / run scripts. All three tools it exposes (`load_skill`, `read_skill_resource`, `run_skill_script`) are registered with `approval_mode="always_require"`, so every skill operation needs approval. To run unattended, pass one of the static auto-approval rules to `ToolApprovalMiddleware` (via `auto_approval_rules`): `SkillsProvider.read_only_tools_auto_approval_rule` approves only the read-only tools (`load_skill`, `read_skill_resource`) while still prompting for `run_skill_script`, and `SkillsProvider.all_tools_auto_approval_rule` approves every skill tool including script execution. Both rules reject any call carrying a `server_label` so they stay scoped to this provider's local tools and never auto-approve a same-named hosted tool. The tool names are also exposed as class constants (`LOAD_SKILL_TOOL_NAME`, `READ_SKILL_RESOURCE_TOOL_NAME`, `RUN_SKILL_SCRIPT_TOOL_NAME`).
- **`SkillsProvider`** - Context provider (extends `ContextProvider`) that discovers file-based skills from `SKILL.md` files and/or accepts code-defined `Skill` instances. Follows progressive disclosure: advertise → load → read resources / run scripts. By default all three tools it exposes (`load_skill`, `read_skill_resource`, `run_skill_script`) are registered with `approval_mode="always_require"`, so every skill operation needs approval. To run unattended, pass one of the static auto-approval rules to `ToolApprovalMiddleware` (via `auto_approval_rules`): `SkillsProvider.read_only_tools_auto_approval_rule` approves only the read-only tools (`load_skill`, `read_skill_resource`) while still prompting for `run_skill_script`, and `SkillsProvider.all_tools_auto_approval_rule` approves every skill tool including script execution. Both rules reject any call carrying a `server_label` so they stay scoped to this provider's local tools and never auto-approve a same-named hosted tool. Alternatively, for trusted skills, the constructor / `from_paths` kwargs `disable_load_skill_approval`, `disable_read_skill_resource_approval`, and `disable_run_skill_script_approval` (all default `False`) opt individual tools out of approval entirely by registering them with `approval_mode="never_require"` (the auto-approval rules only apply to tools that still require approval). The tool names are also exposed as class constants (`LOAD_SKILL_TOOL_NAME`, `READ_SKILL_RESOURCE_TOOL_NAME`, `RUN_SKILL_SCRIPT_TOOL_NAME`).
- **`SkillsSource` decorators** - Skill sources are composable: `SkillsSource` is the abstract base, with concrete sources (`InMemorySkillsSource`, `FileSkillsSource`, `MCPSkillsSource`) and decorators that wrap an inner source — `AggregatingSkillsSource` (concatenate several sources), `FilteringSkillsSource` (predicate filter), `DeduplicatingSkillsSource` (first-wins by name), and `CachingSkillsSource` (cache the inner source's skills list). `DelegatingSkillsSource` is the abstract base for decorators. **Caching lives in the source pipeline, not the provider**: `SkillsProvider` wraps its resolved source in a `CachingSkillsSource` by default (so expensive filesystem/network discovery runs once and is reused), and rebuilds instructions/tools from the cached skills each run. Pass `disable_caching=True` to `SkillsProvider(...)` / `SkillsProvider.from_paths(...)` to skip the wrapping and re-query the source on every run. `CachingSkillsSource` shares a single in-flight fetch across concurrent callers and resets its cache on failure so the next call retries.

### Model Context Protocol (`_mcp.py`)
Expand Down
102 changes: 88 additions & 14 deletions python/packages/core/agent_framework/_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@

from ._feature_stage import ExperimentalFeature, experimental
from ._sessions import ContextProvider
from ._tools import FunctionTool
from ._tools import ApprovalMode, FunctionTool

if TYPE_CHECKING:
from mcp.client.session import ClientSession
Expand Down Expand Up @@ -1850,7 +1850,7 @@ class SkillsProvider(ContextProvider):
and file-based resource reads are guarded against path traversal and
symlink escape. Only use skills from trusted sources.

**Tool approval:** every tool exposed by this provider
**Tool approval:** by default every tool exposed by this provider
(``load_skill``, ``read_skill_resource``, and ``run_skill_script``) is
registered with ``approval_mode="always_require"``, so each skill operation
needs approval. To run unattended, pass one of the static
Expand All @@ -1859,7 +1859,12 @@ class SkillsProvider(ContextProvider):
:meth:`read_only_tools_auto_approval_rule` approves only the read-only tools
(``load_skill`` and ``read_skill_resource``) while still prompting for
``run_skill_script``, and :meth:`all_tools_auto_approval_rule` approves every
skill tool including script execution.
skill tool including script execution. Alternatively, for trusted skills,
set ``disable_load_skill_approval``, ``disable_read_skill_resource_approval``,
and/or ``disable_run_skill_script_approval`` to opt individual tools out of
approval entirely (those tools are registered with
``approval_mode="never_require"`` and are not surfaced as approval requests;
the auto-approval rules only apply to tools that still require approval).

Examples:
File-based factory (recommended for single-source file skills):
Expand Down Expand Up @@ -1949,7 +1954,10 @@ def _is_local_tool_call(function_call: Content) -> bool:
def read_only_tools_auto_approval_rule(function_call: Content) -> bool:
"""Auto-approval rule that approves only the read-only skill tools.

The tools exposed by :class:`SkillsProvider` always require approval.
By default the tools exposed by :class:`SkillsProvider` require
approval. This rule only applies to tools that still require approval;
tools opted out via the ``disable_*_approval`` constructor arguments run
without approval regardless.
Pass this rule to :class:`~agent_framework.ToolApprovalMiddleware` (via
``auto_approval_rules``) to automatically approve the tools that read
skill content (``load_skill`` and ``read_skill_resource``), while still
Expand All @@ -1975,7 +1983,10 @@ def read_only_tools_auto_approval_rule(function_call: Content) -> bool:
def all_tools_auto_approval_rule(function_call: Content) -> bool:
"""Auto-approval rule that approves every skill tool.

The tools exposed by :class:`SkillsProvider` always require approval.
By default the tools exposed by :class:`SkillsProvider` require
approval. This rule only applies to tools that still require approval;
tools opted out via the ``disable_*_approval`` constructor arguments run
without approval regardless.
Pass this rule to :class:`~agent_framework.ToolApprovalMiddleware` (via
``auto_approval_rules``) to automatically approve every skill tool,
including the script execution tool (``run_skill_script``).
Expand All @@ -2001,6 +2012,9 @@ def __init__(
*,
instruction_template: str | None = None,
disable_caching: bool = False,
disable_load_skill_approval: bool = False,
disable_read_skill_resource_approval: bool = False,
disable_run_skill_script_approval: bool = False,
source_id: str | None = None,
) -> None:
"""Initialize a SkillsProvider.
Expand Down Expand Up @@ -2029,14 +2043,33 @@ def __init__(
disable_caching: When ``True``, rebuilds tools and instructions
from the source on every invocation instead of caching
after the first build. Defaults to ``False``.
disable_load_skill_approval: When ``True``, the ``load_skill`` tool
is registered with ``approval_mode="never_require"`` so it runs
without approval. Defaults to ``False`` (approval required).
Only enable this for skills from a trusted source.
disable_read_skill_resource_approval: When ``True``, the
``read_skill_resource`` tool is registered with
``approval_mode="never_require"`` so it runs without approval.
Defaults to ``False`` (approval required). Only enable this for
skills from a trusted source.
disable_run_skill_script_approval: When ``True``, the
``run_skill_script`` tool is registered with
``approval_mode="never_require"`` so it runs without approval.
Defaults to ``False`` (approval required). Only enable this for
skills and scripts from a trusted source.
source_id: Unique identifier for this provider instance.

.. note::

All skill tools require approval. To approve them
By default every skill tool requires approval. To approve them
automatically, pass :meth:`read_only_tools_auto_approval_rule` or
:meth:`all_tools_auto_approval_rule` to
:class:`~agent_framework.ToolApprovalMiddleware`. See
:class:`~agent_framework.ToolApprovalMiddleware`. Alternatively, for
trusted skills, set one or more of
``disable_load_skill_approval``, ``disable_read_skill_resource_approval``,
and ``disable_run_skill_script_approval`` to opt individual tools out
of approval entirely (the auto-approval rules only apply to tools
that still require approval). See
``samples/02-agents/skills/skills_auto_approval/skills_auto_approval.py``
for the auto-approval pattern and
``samples/02-agents/skills/script_approval/script_approval.py`` for
Expand Down Expand Up @@ -2067,6 +2100,9 @@ def __init__(
self._source = source
self._instruction_template = instruction_template
self._disable_caching = disable_caching
self._disable_load_skill_approval = disable_load_skill_approval
self._disable_read_skill_resource_approval = disable_read_skill_resource_approval
self._disable_run_skill_script_approval = disable_run_skill_script_approval

@classmethod
def from_paths(
Expand All @@ -2081,6 +2117,9 @@ def from_paths(
resource_filter: Callable[[str, str], bool] | None = None,
instruction_template: str | None = None,
disable_caching: bool = False,
disable_load_skill_approval: bool = False,
disable_read_skill_resource_approval: bool = False,
disable_run_skill_script_approval: bool = False,
source_id: str | None = None,
) -> _TSkillsProvider:
"""Create a provider from one or more file-based skill directories.
Expand Down Expand Up @@ -2118,17 +2157,31 @@ def from_paths(
disable_caching: When ``True``, rebuilds tools and instructions
from the source on every invocation instead of caching
after the first build.
disable_load_skill_approval: When ``True``, the ``load_skill`` tool
runs without approval. Defaults to ``False``. Only enable this
for skills from a trusted source.
disable_read_skill_resource_approval: When ``True``, the
``read_skill_resource`` tool runs without approval. Defaults to
``False``. Only enable this for skills from a trusted source.
disable_run_skill_script_approval: When ``True``, the
``run_skill_script`` tool runs without approval. Defaults to
``False``. Only enable this for skills and scripts from a
trusted source.
source_id: Unique identifier for this provider instance.

Returns:
A configured :class:`SkillsProvider`.

.. note::

All skill tools require approval. To approve them
By default every skill tool requires approval. To approve them
automatically, pass :meth:`read_only_tools_auto_approval_rule` or
:meth:`all_tools_auto_approval_rule` to
:class:`~agent_framework.ToolApprovalMiddleware`.
:class:`~agent_framework.ToolApprovalMiddleware`. Alternatively, for
trusted skills, set one or more of ``disable_load_skill_approval``,
``disable_read_skill_resource_approval``, and
``disable_run_skill_script_approval`` to opt individual tools out of
approval entirely.
"""
source = DeduplicatingSkillsSource(
FileSkillsSource(
Expand All @@ -2145,6 +2198,9 @@ def from_paths(
source,
instruction_template=instruction_template,
disable_caching=disable_caching,
disable_load_skill_approval=disable_load_skill_approval,
Comment thread
giles17 marked this conversation as resolved.
Outdated
disable_read_skill_resource_approval=disable_read_skill_resource_approval,
disable_run_skill_script_approval=disable_run_skill_script_approval,
source_id=source_id,
)

Expand Down Expand Up @@ -2278,19 +2334,37 @@ async def before_run(
context.extend_instructions(self.source_id, instructions) # type: ignore[arg-type]
context.extend_tools(self.source_id, tools)

@staticmethod
def _approval_mode(approval_disabled: bool) -> ApprovalMode:
"""Return the ``approval_mode`` for a tool given its disable flag.

Args:
approval_disabled: When ``True``, the tool runs without approval.

Returns:
``"never_require"`` when approval is disabled, otherwise
``"always_require"``.
"""
return "never_require" if approval_disabled else "always_require"

def _create_tools(
self,
skills: Sequence[Skill],
) -> list[FunctionTool]:
"""Create the tool definitions for skill interaction.

Always includes ``load_skill``, ``read_skill_resource``, and
``run_skill_script``. Every tool is registered with
``run_skill_script``. By default every tool is registered with
``approval_mode="always_require"`` so each skill operation needs
approval; use :meth:`read_only_tools_auto_approval_rule` or
:meth:`all_tools_auto_approval_rule` with
:class:`~agent_framework.ToolApprovalMiddleware` to approve them
automatically.
automatically. For trusted skills, individual tools can be opted out of
approval entirely via the ``disable_load_skill_approval``,
``disable_read_skill_resource_approval``, and
``disable_run_skill_script_approval`` constructor arguments, in which
case the corresponding tool is registered with
``approval_mode="never_require"``.

Args:
skills: The skills to bind to tool handlers.
Expand All @@ -2315,7 +2389,7 @@ async def _run_script(
name=self.LOAD_SKILL_TOOL_NAME,
description="Loads the full instructions for a specific skill.",
func=_load,
approval_mode="always_require",
approval_mode=self._approval_mode(self._disable_load_skill_approval),
input_model={
"type": "object",
"properties": {
Expand All @@ -2328,7 +2402,7 @@ async def _run_script(
name=self.READ_SKILL_RESOURCE_TOOL_NAME,
description=("Reads a resource associated with a skill, such as references, assets, or dynamic data."),
func=_read_resource,
approval_mode="always_require",
approval_mode=self._approval_mode(self._disable_read_skill_resource_approval),
input_model={
"type": "object",
"properties": {
Expand All @@ -2345,7 +2419,7 @@ async def _run_script(
name=self.RUN_SKILL_SCRIPT_TOOL_NAME,
description="Runs a script associated with a skill.",
func=_run_script,
approval_mode="always_require",
approval_mode=self._approval_mode(self._disable_run_skill_script_approval),
input_model={
"type": "object",
"properties": {
Expand Down
Loading
Loading