Skip to content

Commit 0d4383c

Browse files
Copilotpatniko
andcommitted
Add requiresApproval field to custom tools in Node.js and .NET SDKs
Co-authored-by: patniko <26906478+patniko@users.noreply.github.com>
1 parent 43ac653 commit 0d4383c

6 files changed

Lines changed: 156 additions & 2 deletions

File tree

dotnet/src/Client.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,41 @@ public async Task<ToolCallResponse> OnToolCall(string sessionId,
872872
});
873873
}
874874

875+
// Check if tool requires approval
876+
if (session.ToolRequiresApproval(toolName))
877+
{
878+
try
879+
{
880+
var permissionResult = await session.HandlePermissionRequestAsync(
881+
JsonSerializer.SerializeToElement(new
882+
{
883+
kind = "tool",
884+
toolCallId,
885+
toolName
886+
}));
887+
888+
if (permissionResult.Kind != "approved")
889+
{
890+
return new ToolCallResponse(new ToolResultObject
891+
{
892+
TextResultForLlm = permissionResult.Kind == "denied-interactively-by-user"
893+
? "Tool execution was denied by user."
894+
: "Tool execution was denied.",
895+
ResultType = "denied"
896+
});
897+
}
898+
}
899+
catch
900+
{
901+
// If permission handler fails or is not configured, deny the tool execution
902+
return new ToolCallResponse(new ToolResultObject
903+
{
904+
TextResultForLlm = "Tool execution requires permission but no permission handler is configured.",
905+
ResultType = "denied"
906+
});
907+
}
908+
}
909+
875910
try
876911
{
877912
var invocation = new ToolInvocation

dotnet/src/Session.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public partial class CopilotSession : IAsyncDisposable
4545
{
4646
private readonly HashSet<SessionEventHandler> _eventHandlers = new();
4747
private readonly Dictionary<string, AIFunction> _toolHandlers = new();
48+
private readonly Dictionary<string, bool> _toolRequiresApproval = new();
4849
private readonly JsonRpc _rpc;
4950
private PermissionHandler? _permissionHandler;
5051
private readonly SemaphoreSlim _permissionHandlerLock = new(1, 1);
@@ -255,12 +256,36 @@ internal void DispatchEvent(SessionEvent sessionEvent)
255256
/// Tools allow the assistant to execute custom functions. When the assistant invokes a tool,
256257
/// the corresponding handler is called with the tool arguments.
257258
/// </remarks>
258-
internal void RegisterTools(ICollection<AIFunction> tools)
259+
internal void RegisterTools(ICollection<AIFunction>? tools)
259260
{
260261
_toolHandlers.Clear();
262+
_toolRequiresApproval.Clear();
263+
if (tools == null) return;
264+
261265
foreach (var tool in tools)
262266
{
263267
_toolHandlers.Add(tool.Name, tool);
268+
_toolRequiresApproval[tool.Name] = false;
269+
}
270+
}
271+
272+
/// <summary>
273+
/// Registers custom tool handlers for this session with requiresApproval support.
274+
/// </summary>
275+
/// <param name="tools">A collection of CopilotTools that can be invoked by the assistant.</param>
276+
/// <remarks>
277+
/// Tools allow the assistant to execute custom functions. When the assistant invokes a tool,
278+
/// the corresponding handler is called with the tool arguments.
279+
/// CopilotTools support the RequiresApproval flag for permission handling.
280+
/// </remarks>
281+
internal void RegisterTools(ICollection<CopilotTool> tools)
282+
{
283+
_toolHandlers.Clear();
284+
_toolRequiresApproval.Clear();
285+
foreach (var tool in tools)
286+
{
287+
_toolHandlers.Add(tool.Function.Name, tool.Function);
288+
_toolRequiresApproval[tool.Function.Name] = tool.RequiresApproval;
264289
}
265290
}
266291

@@ -272,6 +297,14 @@ internal void RegisterTools(ICollection<AIFunction> tools)
272297
internal AIFunction? GetTool(string name) =>
273298
_toolHandlers.TryGetValue(name, out var tool) ? tool : null;
274299

300+
/// <summary>
301+
/// Checks if a tool requires approval before execution.
302+
/// </summary>
303+
/// <param name="name">The name of the tool to check.</param>
304+
/// <returns>True if the tool requires approval, false otherwise.</returns>
305+
internal bool ToolRequiresApproval(string name) =>
306+
_toolRequiresApproval.TryGetValue(name, out var requiresApproval) && requiresApproval;
307+
275308
/// <summary>
276309
/// Registers a handler for permission requests.
277310
/// </summary>

dotnet/src/Types.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,33 @@ public class ToolInvocation
8383

8484
public delegate Task<object?> ToolHandler(ToolInvocation invocation);
8585

86+
/// <summary>
87+
/// Wraps an AIFunction with additional metadata for the Copilot SDK.
88+
/// </summary>
89+
public class CopilotTool
90+
{
91+
/// <summary>
92+
/// The underlying AIFunction that handles tool execution.
93+
/// </summary>
94+
public AIFunction Function { get; set; } = null!;
95+
96+
/// <summary>
97+
/// Controls whether the tool requires user approval before execution.
98+
/// When true, the OnPermissionRequest handler will be called before invoking the tool.
99+
/// When false or not specified, the tool executes without requesting permission.
100+
/// </summary>
101+
public bool RequiresApproval { get; set; }
102+
103+
/// <summary>
104+
/// Creates a CopilotTool from an AIFunction with optional requiresApproval flag.
105+
/// </summary>
106+
/// <param name="function">The AIFunction to wrap.</param>
107+
/// <param name="requiresApproval">Whether the tool requires approval before execution.</param>
108+
/// <returns>A CopilotTool wrapping the provided function.</returns>
109+
public static implicit operator CopilotTool(AIFunction function) =>
110+
new() { Function = function, RequiresApproval = false };
111+
}
112+
86113
public class PermissionRequest
87114
{
88115
[JsonPropertyName("kind")]

nodejs/src/client.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,36 @@ export class CopilotClient {
985985
return { result: this.buildUnsupportedToolResult(params.toolName) };
986986
}
987987

988+
// Check if tool requires approval
989+
if (session.toolRequiresApprovalCheck(params.toolName)) {
990+
try {
991+
const permissionResult = await session._handlePermissionRequest({
992+
kind: "tool",
993+
toolCallId: params.toolCallId,
994+
toolName: params.toolName,
995+
});
996+
997+
if (permissionResult.kind !== "approved") {
998+
return {
999+
result: {
1000+
textResultForLlm: `Tool execution was ${permissionResult.kind === "denied-interactively-by-user" ? "denied by user" : "denied"}.`,
1001+
resultType: "denied",
1002+
toolTelemetry: {},
1003+
},
1004+
};
1005+
}
1006+
} catch (error) {
1007+
// If permission handler fails or is not configured, deny the tool execution
1008+
return {
1009+
result: {
1010+
textResultForLlm: "Tool execution requires permission but no permission handler is configured.",
1011+
resultType: "denied",
1012+
toolTelemetry: {},
1013+
},
1014+
};
1015+
}
1016+
}
1017+
9881018
return await this.executeToolCall(handler, params);
9891019
}
9901020

nodejs/src/session.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export type AssistantMessageEvent = Extract<SessionEvent, { type: "assistant.mes
5050
export class CopilotSession {
5151
private eventHandlers: Set<SessionEventHandler> = new Set();
5252
private toolHandlers: Map<string, ToolHandler> = new Map();
53+
private toolRequiresApproval: Map<string, boolean> = new Map();
5354
private permissionHandler?: PermissionHandler;
5455

5556
/**
@@ -238,12 +239,14 @@ export class CopilotSession {
238239
*/
239240
registerTools(tools?: Tool[]): void {
240241
this.toolHandlers.clear();
242+
this.toolRequiresApproval.clear();
241243
if (!tools) {
242244
return;
243245
}
244246

245247
for (const tool of tools) {
246248
this.toolHandlers.set(tool.name, tool.handler);
249+
this.toolRequiresApproval.set(tool.name, tool.requiresApproval ?? false);
247250
}
248251
}
249252

@@ -258,6 +261,17 @@ export class CopilotSession {
258261
return this.toolHandlers.get(name);
259262
}
260263

264+
/**
265+
* Checks if a tool requires approval before execution.
266+
*
267+
* @param name - The name of the tool to check
268+
* @returns True if the tool requires approval, false otherwise
269+
* @internal This method is for internal use by the SDK.
270+
*/
271+
toolRequiresApprovalCheck(name: string): boolean {
272+
return this.toolRequiresApproval.get(name) ?? false;
273+
}
274+
261275
/**
262276
* Registers a handler for permission requests.
263277
*

nodejs/src/types.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ export interface Tool<TArgs = unknown> {
131131
description?: string;
132132
parameters?: ZodSchema<TArgs> | Record<string, unknown>;
133133
handler: ToolHandler<TArgs>;
134+
/**
135+
* Controls whether the tool requires user approval before execution.
136+
* When true, the OnPermissionRequest handler will be called before invoking the tool.
137+
* When false or undefined, the tool executes without requesting permission.
138+
* @default false
139+
*/
140+
requiresApproval?: boolean;
134141
}
135142

136143
/**
@@ -143,6 +150,13 @@ export function defineTool<T = unknown>(
143150
description?: string;
144151
parameters?: ZodSchema<T> | Record<string, unknown>;
145152
handler: ToolHandler<T>;
153+
/**
154+
* Controls whether the tool requires user approval before execution.
155+
* When true, the OnPermissionRequest handler will be called before invoking the tool.
156+
* When false or undefined, the tool executes without requesting permission.
157+
* @default false
158+
*/
159+
requiresApproval?: boolean;
146160
}
147161
): Tool<T> {
148162
return { name, ...config };
@@ -196,8 +210,9 @@ export type SystemMessageConfig = SystemMessageAppendConfig | SystemMessageRepla
196210
* Permission request types from the server
197211
*/
198212
export interface PermissionRequest {
199-
kind: "shell" | "write" | "mcp" | "read" | "url";
213+
kind: "shell" | "write" | "mcp" | "read" | "url" | "tool";
200214
toolCallId?: string;
215+
toolName?: string;
201216
[key: string]: unknown;
202217
}
203218

0 commit comments

Comments
 (0)