Skip to content

Commit e7e0521

Browse files
Allow tools to set skipPermission
1 parent df59a0e commit e7e0521

13 files changed

Lines changed: 174 additions & 2 deletions

File tree

dotnet/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,24 @@ var session = await client.CreateSessionAsync(new SessionConfig
449449
});
450450
```
451451

452+
#### Skipping Permission Prompts
453+
454+
Set `skip_permission` in the tool's `AdditionalProperties` to allow it to execute without triggering a permission prompt:
455+
456+
```csharp
457+
var safeLookup = AIFunctionFactory.Create(
458+
async ([Description("Lookup ID")] string id) => {
459+
// your logic
460+
},
461+
"safe_lookup",
462+
"A read-only lookup that needs no confirmation",
463+
new AIFunctionFactoryOptions
464+
{
465+
AdditionalProperties = new ReadOnlyDictionary<string, object?>(
466+
new Dictionary<string, object?> { ["skip_permission"] = true })
467+
});
468+
```
469+
452470
### System Message Customization
453471

454472
Control the system prompt using `SystemMessage` in session config:

dotnet/src/Client.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,13 +1470,16 @@ internal record ToolDefinition(
14701470
string Name,
14711471
string? Description,
14721472
JsonElement Parameters, /* JSON schema */
1473-
bool? OverridesBuiltInTool = null)
1473+
bool? OverridesBuiltInTool = null,
1474+
bool? SkipPermission = null)
14741475
{
14751476
public static ToolDefinition FromAIFunction(AIFunction function)
14761477
{
14771478
var overrides = function.AdditionalProperties.TryGetValue("is_override", out var val) && val is true;
1479+
var skipPerm = function.AdditionalProperties.TryGetValue("skip_permission", out var skipVal) && skipVal is true;
14781480
return new ToolDefinition(function.Name, function.Description, function.JsonSchema,
1479-
overrides ? true : null);
1481+
overrides ? true : null,
1482+
skipPerm ? true : null);
14801483
}
14811484
}
14821485

go/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,18 @@ editFile := copilot.DefineTool("edit_file", "Custom file editor with project-spe
281281
editFile.OverridesBuiltInTool = true
282282
```
283283

284+
#### Skipping Permission Prompts
285+
286+
Set `SkipPermission = true` on a tool to allow it to execute without triggering a permission prompt:
287+
288+
```go
289+
safeLookup := copilot.DefineTool("safe_lookup", "A read-only lookup that needs no confirmation",
290+
func(params LookupParams, inv copilot.ToolInvocation) (any, error) {
291+
// your logic
292+
})
293+
safeLookup.SkipPermission = true
294+
```
295+
284296
## Streaming
285297

286298
Enable streaming to receive assistant response chunks as they're generated:

go/client_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,47 @@ func TestOverridesBuiltInTool(t *testing.T) {
509509
})
510510
}
511511

512+
func TestSkipPermission(t *testing.T) {
513+
t.Run("SkipPermission is serialized in tool definition", func(t *testing.T) {
514+
tool := Tool{
515+
Name: "my_tool",
516+
Description: "A tool that skips permission",
517+
SkipPermission: true,
518+
Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil },
519+
}
520+
data, err := json.Marshal(tool)
521+
if err != nil {
522+
t.Fatalf("failed to marshal: %v", err)
523+
}
524+
var m map[string]any
525+
if err := json.Unmarshal(data, &m); err != nil {
526+
t.Fatalf("failed to unmarshal: %v", err)
527+
}
528+
if v, ok := m["skipPermission"]; !ok || v != true {
529+
t.Errorf("expected skipPermission=true, got %v", m)
530+
}
531+
})
532+
533+
t.Run("SkipPermission omitted when false", func(t *testing.T) {
534+
tool := Tool{
535+
Name: "custom_tool",
536+
Description: "A custom tool",
537+
Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil },
538+
}
539+
data, err := json.Marshal(tool)
540+
if err != nil {
541+
t.Fatalf("failed to marshal: %v", err)
542+
}
543+
var m map[string]any
544+
if err := json.Unmarshal(data, &m); err != nil {
545+
t.Fatalf("failed to unmarshal: %v", err)
546+
}
547+
if _, ok := m["skipPermission"]; ok {
548+
t.Errorf("expected skipPermission to be omitted, got %v", m)
549+
}
550+
})
551+
}
552+
512553
func TestClient_CreateSession_RequiresPermissionHandler(t *testing.T) {
513554
t.Run("returns error when config is nil", func(t *testing.T) {
514555
client := NewClient(nil)

go/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ type Tool struct {
414414
Description string `json:"description,omitempty"`
415415
Parameters map[string]any `json:"parameters,omitempty"`
416416
OverridesBuiltInTool bool `json:"overridesBuiltInTool,omitempty"`
417+
SkipPermission bool `json:"skipPermission,omitempty"`
417418
Handler ToolHandler `json:"-"`
418419
}
419420

nodejs/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,19 @@ defineTool("edit_file", {
426426
})
427427
```
428428

429+
#### Skipping Permission Prompts
430+
431+
Set `skipPermission: true` on a tool definition to allow it to execute without triggering a permission prompt:
432+
433+
```ts
434+
defineTool("safe_lookup", {
435+
description: "A read-only lookup that needs no confirmation",
436+
parameters: z.object({ id: z.string() }),
437+
skipPermission: true,
438+
handler: async ({ id }) => { /* your logic */ },
439+
})
440+
```
441+
429442
### System Message Customization
430443

431444
Control the system prompt using `systemMessage` in session config:

nodejs/src/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,7 @@ export class CopilotClient {
580580
description: tool.description,
581581
parameters: toJsonSchema(tool.parameters),
582582
overridesBuiltInTool: tool.overridesBuiltInTool,
583+
skipPermission: tool.skipPermission,
583584
})),
584585
systemMessage: config.systemMessage,
585586
availableTools: config.availableTools,
@@ -682,6 +683,7 @@ export class CopilotClient {
682683
description: tool.description,
683684
parameters: toJsonSchema(tool.parameters),
684685
overridesBuiltInTool: tool.overridesBuiltInTool,
686+
skipPermission: tool.skipPermission,
685687
})),
686688
provider: config.provider,
687689
requestPermission: true,

nodejs/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ export interface Tool<TArgs = unknown> {
167167
* will return an error.
168168
*/
169169
overridesBuiltInTool?: boolean;
170+
/**
171+
* When true, the tool can execute without a permission prompt.
172+
*/
173+
skipPermission?: boolean;
170174
}
171175

172176
/**
@@ -180,6 +184,7 @@ export function defineTool<T = unknown>(
180184
parameters?: ZodSchema<T> | Record<string, unknown>;
181185
handler: ToolHandler<T>;
182186
overridesBuiltInTool?: boolean;
187+
skipPermission?: boolean;
183188
}
184189
): Tool<T> {
185190
return { name, ...config };

nodejs/test/client.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,64 @@ describe("CopilotClient", () => {
378378
});
379379
});
380380

381+
describe("skipPermission in tool definitions", () => {
382+
it("sends skipPermission in tool definition on session.create", async () => {
383+
const client = new CopilotClient();
384+
await client.start();
385+
onTestFinished(() => client.forceStop());
386+
387+
const spy = vi.spyOn((client as any).connection!, "sendRequest");
388+
await client.createSession({
389+
onPermissionRequest: approveAll,
390+
tools: [
391+
{
392+
name: "my_tool",
393+
description: "a tool that skips permission",
394+
handler: async () => "ok",
395+
skipPermission: true,
396+
},
397+
],
398+
});
399+
400+
const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any;
401+
expect(payload.tools).toEqual([
402+
expect.objectContaining({ name: "my_tool", skipPermission: true }),
403+
]);
404+
});
405+
406+
it("sends skipPermission in tool definition on session.resume", async () => {
407+
const client = new CopilotClient();
408+
await client.start();
409+
onTestFinished(() => client.forceStop());
410+
411+
const session = await client.createSession({ onPermissionRequest: approveAll });
412+
// Mock sendRequest to capture the call without hitting the runtime
413+
const spy = vi
414+
.spyOn((client as any).connection!, "sendRequest")
415+
.mockImplementation(async (method: string, params: any) => {
416+
if (method === "session.resume") return { sessionId: params.sessionId };
417+
throw new Error(`Unexpected method: ${method}`);
418+
});
419+
await client.resumeSession(session.sessionId, {
420+
onPermissionRequest: approveAll,
421+
tools: [
422+
{
423+
name: "my_tool",
424+
description: "a tool that skips permission",
425+
handler: async () => "ok",
426+
skipPermission: true,
427+
},
428+
],
429+
});
430+
431+
const payload = spy.mock.calls.find((c) => c[0] === "session.resume")![1] as any;
432+
expect(payload.tools).toEqual([
433+
expect.objectContaining({ name: "my_tool", skipPermission: true }),
434+
]);
435+
spy.mockRestore();
436+
});
437+
});
438+
381439
describe("agent parameter in session creation", () => {
382440
it("forwards agent in session.create request", async () => {
383441
const client = new CopilotClient();

python/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,16 @@ async def edit_file(params: EditFileParams) -> str:
232232
# your logic
233233
```
234234

235+
#### Skipping Permission Prompts
236+
237+
Set `skip_permission=True` on a tool definition to allow it to execute without triggering a permission prompt:
238+
239+
```python
240+
@define_tool(name="safe_lookup", description="A read-only lookup that needs no confirmation", skip_permission=True)
241+
async def safe_lookup(params: LookupParams) -> str:
242+
# your logic
243+
```
244+
235245
## Image Support
236246

237247
The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path:

0 commit comments

Comments
 (0)