Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
<PackageVersion Include="Dapr.Testcontainers" Version="1.18.4" />
<PackageVersion Include="Dapr.Workflow" Version="1.18.4" />
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.4" />
<PackageVersion Include="Microsoft.Agents.AI" Version="1.9.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.6.0" />
<PackageVersion Include="Microsoft.Agents.AI" Version="1.23.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.10.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.8" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="8.0.28" Condition="'$(TargetFramework)' == 'net8.0'" />
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ builder.Services.AddDaprAgents()
description: "Tells a short, work-appropriate joke on request.",
instructions: "When asked for a joke, tell exactly one short, clean joke."))
.UseSkill(new GreetingSkill()) // Class-based: AgentClassSkill<T>
.UseScriptApproval()); // Require approval before running scripts
.UseOptions(o => o.DisableRunSkillScriptApproval = false)); // Require approval before running scripts

var app = builder.Build();
```
Expand All @@ -167,9 +167,13 @@ Any `AIContextProvider` — not just skills — can be attached to an agent the
`WithContextProviders(...)`.

### Script approval
Skill-bundled scripts can require human approval before they run
(`AgentSkillsProviderBuilder.UseScriptApproval()`). Implement `IToolApprovalHandler` and register it
*before* calling `AddDaprAgents()` to decide whether a given call is allowed to proceed — without one
Skill tools attached via `WithSkills(...)` run without approval by default. To require human
approval before a skill-bundled script runs, set `o.DisableRunSkillScriptApproval = false` in
`AgentSkillsProviderBuilder.UseOptions` (the same applies to `DisableLoadSkillApproval` and
`DisableReadSkillResourceApproval`). An `AgentSkillsProvider` you construct yourself and attach via
`WithContextProviders(...)` keeps MAF's own default, where all three tools require approval.

Implement `IToolApprovalHandler` and register it *before* calling `AddDaprAgents()` to decide whether a given call is allowed to proceed — without one
registered, every approval-required call is denied by default:

```csharp
Expand Down
4 changes: 2 additions & 2 deletions examples/SkillsDemo/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@
description: "Tells a short, work-appropriate joke on request.",
instructions: "When asked for a joke, tell exactly one short, clean joke. Do not explain it."))
// Class-based: instructions + an [AgentSkillScript]-attributed method live together on one
// C# class. UseScriptApproval() means run_skill_script for THIS script (and any other
// C# class. Re-enabling script approval means run_skill_script for THIS script (and any other
// skill's scripts) is gated by IToolApprovalHandler before it actually runs.
.UseSkill(new GreetingSkill())
.UseScriptApproval());
.UseOptions(o => o.DisableRunSkillScriptApproval = false));

var app = builder.Build();

Expand Down
2 changes: 1 addition & 1 deletion examples/SkillsDemo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ at runtime, discovered via all three mechanisms MAF supports and mixed together
- **Class-based** — a `greeting-formalizer` skill (`GreetingSkill : AgentClassSkill<GreetingSkill>`) whose
script is a plain C# method attributed with `[AgentSkillScript]`.

`UseScriptApproval()` also gates skill scripts (like `greeting-formalizer`'s) behind human approval —
`UseOptions(o => o.DisableRunSkillScriptApproval = false)` also gates skill scripts (like `greeting-formalizer`'s) behind human approval —
see `IToolApprovalHandler` and the `ConsoleApprovingToolApprovalHandler` in `Program.cs`.

## Prerequisites
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace Diagrid.AI.Microsoft.AgentFramework.Abstractions;
/// <summary>
/// Decides whether a tool call that requires human approval — i.e. one whose resolved
/// <see cref="AIFunction"/> is a <see cref="ApprovalRequiredAIFunction"/>,
/// such as an MAF skill script when <c>AgentSkillsProviderOptions.ScriptApproval</c> is enabled —
/// such as an MAF skill script when <c>AgentSkillsProviderOptions.DisableRunSkillScriptApproval</c> is false —
/// is allowed to run.
/// </summary>
/// <remarks>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,23 +95,32 @@ internal IAgentsBuilder WithAgentRegistration(AgentFactoryRegistration registrat
}

/// <summary>
/// Traverses the <see cref="IChatClient"/> pipeline and returns the first client
/// that is NOT a <see cref="FunctionInvokingChatClient"/>.
/// This gives us the raw client suitable for single-turn LLM calls.
/// Traverses the <see cref="IChatClient"/> pipeline and returns the client directly below the
/// innermost <see cref="FunctionInvokingChatClient"/>, or <paramref name="client"/> itself when the
/// pipeline has none. This gives us the raw client suitable for single-turn LLM calls, so tools
/// run in their own workflow activity instead of inside the LLM call.
/// <see cref="ChatClientAgent"/> may place decorators above the <see cref="FunctionInvokingChatClient"/>
/// (e.g. its approval decorators), so the whole <see cref="DelegatingChatClient"/> chain is walked.
/// Uses <see cref="UnsafeAccessorAttribute"/> for AOT-safe access to the protected
/// <see cref="DelegatingChatClient.InnerClient"/> property.
/// </summary>
internal static IChatClient UnwrapFunctionInvoking(IChatClient client)
{
while (client is FunctionInvokingChatClient fic)
var result = client;
var current = client;
while (current is DelegatingChatClient delegating)
{
var inner = GetInnerClient(fic);
if (inner is null || ReferenceEquals(inner, client))
var inner = GetInnerClient(delegating);
if (inner is null || ReferenceEquals(inner, current))
break;
client = inner;

if (current is FunctionInvokingChatClient)
result = inner;

current = inner;
}

return client;
return result;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ public static IAgentsBuilder WithContextProviders(

/// <summary>
/// Attaches an MAF skills provider built from the given <see cref="AgentSkill"/> instances to the
/// named agent — covers file-based (<c>AgentFileSkill</c>, via <c>AgentSkillsProvider(skillPath)</c>
/// on the other overload), inline (<see cref="AgentInlineSkill"/>), and class-based
/// named agent — covers file-based (<c>AgentFileSkill</c>, via <c>UseFileSkills(...)</c>
/// on the builder overload), inline (<see cref="AgentInlineSkill"/>), and class-based
/// (<c>AgentClassSkill&lt;T&gt;</c>) skill sources, since they all derive from <see cref="AgentSkill"/>.
/// </summary>
/// <param name="builder">The agents builder.</param>
Expand All @@ -117,13 +117,17 @@ public static IAgentsBuilder WithSkills(
ArgumentException.ThrowIfNullOrWhiteSpace(agentName);
ArgumentNullException.ThrowIfNull(skills);

return builder.WithContextProviders(agentName, new AgentSkillsProvider(skills));
var options = new AgentSkillsProviderOptions();
DisableSkillToolApprovals(options);
return builder.WithContextProviders(agentName, new AgentSkillsProvider(skills, options));
}

/// <summary>
/// Attaches an MAF skills provider to the named agent, built via <see cref="AgentSkillsProviderBuilder"/>.
/// Use this overload to mix file-based, inline, and class-based skill sources, configure script
/// approval (<c>UseScriptApproval()</c>), a custom file script runner, filters, or prompt template.
/// approval, a custom file script runner, filters, or prompt template. Skill tools run without
/// approval unless the callback re-enables it, e.g.
/// <c>UseOptions(o =&gt; o.DisableRunSkillScriptApproval = false)</c>.
/// </summary>
/// <param name="builder">The agents builder.</param>
/// <param name="agentName">The name of the agent to attach the skills to.</param>
Expand All @@ -139,12 +143,22 @@ public static IAgentsBuilder WithSkills(
ArgumentException.ThrowIfNullOrWhiteSpace(agentName);
ArgumentNullException.ThrowIfNull(configureSkills);

var skillsBuilder = new AgentSkillsProviderBuilder();
var skillsBuilder = new AgentSkillsProviderBuilder().UseOptions(DisableSkillToolApprovals);
configureSkills(skillsBuilder);

return builder.WithContextProviders(agentName, skillsBuilder.Build());
}

// MAF 1.23 requires approval for every skill tool by default; keep the earlier behaviour,
// where skill tools need no approval unless the caller opts in.
[Experimental("MAAI001")]
private static void DisableSkillToolApprovals(AgentSkillsProviderOptions options)
{
options.DisableLoadSkillApproval = true;
options.DisableReadSkillResourceApproval = true;
options.DisableRunSkillScriptApproval = true;
}

/// <summary>
/// Registers a keyed <see cref="DaprChatClient"/> and an agent that uses it.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public static IAgentsBuilder AddDaprAgents(
services.AddSingleton<ToolRegistry>();

// Safe-by-default approval gate for tools that require human approval (e.g. an MAF skill
// script when AgentSkillsProviderOptions.ScriptApproval is enabled) — denies until a host
// script when AgentSkillsProviderOptions.DisableRunSkillScriptApproval is false) — denies until a host
// registers its own IToolApprovalHandler (register it BEFORE calling AddDaprAgents()).
services.TryAddSingleton<IToolApprovalHandler, DenyingToolApprovalHandler>();

Expand Down
12 changes: 8 additions & 4 deletions src/Diagrid.AI.Microsoft.AgentFramework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ builder.Services.AddDaprAgents()
description: "Tells a short, work-appropriate joke on request.",
instructions: "When asked for a joke, tell exactly one short, clean joke."))
.UseSkill(new GreetingSkill()) // Class-based: AgentClassSkill<T>
.UseScriptApproval()); // Require approval before running scripts
.UseOptions(o => o.DisableRunSkillScriptApproval = false)); // Require approval before running scripts

var app = builder.Build();
```
Expand All @@ -167,9 +167,13 @@ Any `AIContextProvider` — not just skills — can be attached to an agent the
`WithContextProviders(...)`.

### Script approval
Skill-bundled scripts can require human approval before they run
(`AgentSkillsProviderBuilder.UseScriptApproval()`). Implement `IToolApprovalHandler` and register it
*before* calling `AddDaprAgents()` to decide whether a given call is allowed to proceed — without one
Skill tools attached via `WithSkills(...)` run without approval by default. To require human
approval before a skill-bundled script runs, set `o.DisableRunSkillScriptApproval = false` in
`AgentSkillsProviderBuilder.UseOptions` (the same applies to `DisableLoadSkillApproval` and
`DisableReadSkillResourceApproval`). An `AgentSkillsProvider` you construct yourself and attach via
`WithContextProviders(...)` keeps MAF's own default, where all three tools require approval.

Implement `IToolApprovalHandler` and register it *before* calling `AddDaprAgents()` to decide whether a given call is allowed to proceed — without one
registered, every approval-required call is denied by default:

```csharp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public void WithSkills_Configure_InvokesCallbackWithBuilder_AndAttachesResult()
builder.WithSkills("configured-agent", b =>
{
configured = true;
b.UseSkill(MakeSkill("s")).UseScriptApproval();
b.UseSkill(MakeSkill("s")).UseOptions(o => o.DisableRunSkillScriptApproval = false);
});

var serviceProvider = services.BuildServiceProvider();
Expand All @@ -244,10 +244,76 @@ public void WithSkills_Configure_InvokesCallbackWithBuilder_AndAttachesResult()
Assert.IsType<AgentSkillsProvider>(Assert.Single(config!.ContextProviders!));
}

[Fact]
public async Task WithSkills_Array_SkillToolsDoNotRequireApproval()
{
var tools = await ResolveSkillToolsAsync(b => b.WithSkills("agent", MakeSkill("s")));

Assert.Equal(SkillToolNames, tools.Select(t => t.Name).Order());
Assert.DoesNotContain(tools, t => t is ApprovalRequiredAIFunction);
}

[Fact]
public async Task WithSkills_Configure_SkillToolsDoNotRequireApprovalByDefault()
{
var tools = await ResolveSkillToolsAsync(b => b.WithSkills("agent", s => s.UseSkill(MakeSkill("s"))));

Assert.Equal(SkillToolNames, tools.Select(t => t.Name).Order());
Assert.DoesNotContain(tools, t => t is ApprovalRequiredAIFunction);
}

[Fact]
public async Task WithSkills_Configure_CallerCanReEnableScriptApproval()
{
var tools = await ResolveSkillToolsAsync(b => b.WithSkills("agent", s => s
.UseSkill(MakeSkill("s"))
.UseOptions(o => o.DisableRunSkillScriptApproval = false)));

var gated = Assert.Single(tools, t => t is ApprovalRequiredAIFunction);
Assert.Equal("run_skill_script", gated.Name);
}

[Fact]
public async Task WithSkills_Configure_CallerCanReEnableLoadAndReadApproval()
{
var tools = await ResolveSkillToolsAsync(b => b.WithSkills("agent", s => s
.UseSkill(MakeSkill("s"))
.UseOptions(o =>
{
o.DisableLoadSkillApproval = false;
o.DisableReadSkillResourceApproval = false;
})));

var gated = tools.Where(t => t is ApprovalRequiredAIFunction).Select(t => t.Name).Order();
Assert.Equal(["load_skill", "read_skill_resource"], gated);
}

// =========================================================================
// Helpers
// =========================================================================

private static readonly string[] SkillToolNames = ["load_skill", "read_skill_resource", "run_skill_script"];

private static async Task<IList<AITool>> ResolveSkillToolsAsync(Action<IAgentsBuilder> configure)
{
var services = new ServiceCollection();
var chatClient = new TestChatClient();
services.AddSingleton<IChatClient>(chatClient);
var builder = services.AddDaprAgents();
builder.WithAgent("agent", "Be helpful.");
configure(builder);

var serviceProvider = services.BuildServiceProvider();
FindRegistration(services, "agent").Factory(serviceProvider);
var config = serviceProvider.GetRequiredService<ChatClientRegistry>().Get("agent");
var provider = Assert.IsType<AgentSkillsProvider>(Assert.Single(config!.ContextProviders!));

var agent = new ChatClientAgent(chatClient);
var session = await agent.CreateSessionAsync();
var context = await provider.InvokingAsync(new AIContextProvider.InvokingContext(agent, session, new AIContext()));
return context.Tools!.ToList();
}

private static AgentInlineSkill MakeSkill(string name, string? description = null) =>
new(name: name, description: description ?? $"Skill '{name}'.", instructions: $"Follow the '{name}' skill's guidance.");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,32 @@ public void UnwrapFunctionInvoking_NestedFunctionInvokingWrappers_ReturnsInnermo
Assert.Same(innermost, result);
}

[Fact]
public void UnwrapFunctionInvoking_DecoratorAboveFunctionInvoking_ReturnsClientBelowIt()
{
var leaf = new Mock<IChatClient>().Object;
var outer = new PassThroughChatClient(new FunctionInvokingChatClient(leaf));

var result = DaprAgentsBuilder.UnwrapFunctionInvoking(outer);

Assert.Same(leaf, result);
}

[Fact]
public void UnwrapFunctionInvoking_ChatClientAgentPipeline_HasNoFunctionInvokingClient()
{
// ChatClientAgent places its own decorators above FunctionInvokingChatClient; the
// unwrapped client must still bypass it, or tools run inside the LLM activity.
var leaf = new Mock<IChatClient>().Object;
var agent = leaf.AsAIAgent(instructions: "test", name: "unwrap-agent");

var result = DaprAgentsBuilder.UnwrapFunctionInvoking(agent.ChatClient);

Assert.Null(result.GetService<FunctionInvokingChatClient>());
}

private sealed class PassThroughChatClient(IChatClient inner) : DelegatingChatClient(inner);

// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
Expand Down
Loading