Skip to content
Merged
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
18 changes: 18 additions & 0 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,24 @@ var session = await client.CreateSessionAsync(new SessionConfig
});
```

#### Skipping Permission Prompts

Set `skip_permission` in the tool's `AdditionalProperties` to allow it to execute without triggering a permission prompt:

```csharp
var safeLookup = AIFunctionFactory.Create(
async ([Description("Lookup ID")] string id) => {
// your logic
},
"safe_lookup",
"A read-only lookup that needs no confirmation",
new AIFunctionFactoryOptions
{
AdditionalProperties = new ReadOnlyDictionary<string, object?>(
new Dictionary<string, object?> { ["skip_permission"] = true })
});
```

### System Message Customization

Control the system prompt using `SystemMessage` in session config:
Expand Down
7 changes: 5 additions & 2 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1470,13 +1470,16 @@ internal record ToolDefinition(
string Name,
string? Description,
JsonElement Parameters, /* JSON schema */
bool? OverridesBuiltInTool = null)
bool? OverridesBuiltInTool = null,
bool? SkipPermission = null)
{
public static ToolDefinition FromAIFunction(AIFunction function)
{
var overrides = function.AdditionalProperties.TryGetValue("is_override", out var val) && val is true;
var skipPerm = function.AdditionalProperties.TryGetValue("skip_permission", out var skipVal) && skipVal is true;
return new ToolDefinition(function.Name, function.Description, function.JsonSchema,
overrides ? true : null);
overrides ? true : null,
skipPerm ? true : null);
}
}

Expand Down
12 changes: 12 additions & 0 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,18 @@ editFile := copilot.DefineTool("edit_file", "Custom file editor with project-spe
editFile.OverridesBuiltInTool = true
```

#### Skipping Permission Prompts

Set `SkipPermission = true` on a tool to allow it to execute without triggering a permission prompt:

```go
safeLookup := copilot.DefineTool("safe_lookup", "A read-only lookup that needs no confirmation",
func(params LookupParams, inv copilot.ToolInvocation) (any, error) {
// your logic
})
safeLookup.SkipPermission = true
```

## Streaming

Enable streaming to receive assistant response chunks as they're generated:
Expand Down
41 changes: 41 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,47 @@ func TestOverridesBuiltInTool(t *testing.T) {
})
}

func TestSkipPermission(t *testing.T) {
t.Run("SkipPermission is serialized in tool definition", func(t *testing.T) {
tool := Tool{
Name: "my_tool",
Description: "A tool that skips permission",
SkipPermission: true,
Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil },
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if v, ok := m["skipPermission"]; !ok || v != true {
t.Errorf("expected skipPermission=true, got %v", m)
}
})

t.Run("SkipPermission omitted when false", func(t *testing.T) {
tool := Tool{
Name: "custom_tool",
Description: "A custom tool",
Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil },
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if _, ok := m["skipPermission"]; ok {
t.Errorf("expected skipPermission to be omitted, got %v", m)
}
})
}

func TestClient_CreateSession_RequiresPermissionHandler(t *testing.T) {
t.Run("returns error when config is nil", func(t *testing.T) {
client := NewClient(nil)
Expand Down
1 change: 1 addition & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ type Tool struct {
Description string `json:"description,omitempty"`
Parameters map[string]any `json:"parameters,omitempty"`
OverridesBuiltInTool bool `json:"overridesBuiltInTool,omitempty"`
SkipPermission bool `json:"skipPermission,omitempty"`
Handler ToolHandler `json:"-"`
}

Expand Down
13 changes: 13 additions & 0 deletions nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,19 @@ defineTool("edit_file", {
})
```

#### Skipping Permission Prompts

Set `skipPermission: true` on a tool definition to allow it to execute without triggering a permission prompt:

```ts
defineTool("safe_lookup", {
description: "A read-only lookup that needs no confirmation",
parameters: z.object({ id: z.string() }),
skipPermission: true,
handler: async ({ id }) => { /* your logic */ },
})
```

### System Message Customization

Control the system prompt using `systemMessage` in session config:
Expand Down
2 changes: 2 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ export class CopilotClient {
description: tool.description,
parameters: toJsonSchema(tool.parameters),
overridesBuiltInTool: tool.overridesBuiltInTool,
skipPermission: tool.skipPermission,
})),
systemMessage: config.systemMessage,
availableTools: config.availableTools,
Expand Down Expand Up @@ -682,6 +683,7 @@ export class CopilotClient {
description: tool.description,
parameters: toJsonSchema(tool.parameters),
overridesBuiltInTool: tool.overridesBuiltInTool,
skipPermission: tool.skipPermission,
})),
provider: config.provider,
requestPermission: true,
Expand Down
5 changes: 5 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ export interface Tool<TArgs = unknown> {
* will return an error.
*/
overridesBuiltInTool?: boolean;
/**
* When true, the tool can execute without a permission prompt.
*/
skipPermission?: boolean;
}

/**
Expand All @@ -180,6 +184,7 @@ export function defineTool<T = unknown>(
parameters?: ZodSchema<T> | Record<string, unknown>;
handler: ToolHandler<T>;
overridesBuiltInTool?: boolean;
skipPermission?: boolean;
}
): Tool<T> {
return { name, ...config };
Expand Down
58 changes: 58 additions & 0 deletions nodejs/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,64 @@ describe("CopilotClient", () => {
});
});

describe("skipPermission in tool definitions", () => {
it("sends skipPermission in tool definition on session.create", async () => {
const client = new CopilotClient();
await client.start();
onTestFinished(() => client.forceStop());

const spy = vi.spyOn((client as any).connection!, "sendRequest");
await client.createSession({
onPermissionRequest: approveAll,
tools: [
{
name: "my_tool",
description: "a tool that skips permission",
handler: async () => "ok",
skipPermission: true,
},
],
});

const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any;
expect(payload.tools).toEqual([
expect.objectContaining({ name: "my_tool", skipPermission: true }),
]);
});

it("sends skipPermission in tool definition on session.resume", async () => {
const client = new CopilotClient();
await client.start();
onTestFinished(() => client.forceStop());

const session = await client.createSession({ onPermissionRequest: approveAll });
// Mock sendRequest to capture the call without hitting the runtime
const spy = vi
.spyOn((client as any).connection!, "sendRequest")
.mockImplementation(async (method: string, params: any) => {
if (method === "session.resume") return { sessionId: params.sessionId };
throw new Error(`Unexpected method: ${method}`);
});
await client.resumeSession(session.sessionId, {
onPermissionRequest: approveAll,
tools: [
{
name: "my_tool",
description: "a tool that skips permission",
handler: async () => "ok",
skipPermission: true,
},
],
});

const payload = spy.mock.calls.find((c) => c[0] === "session.resume")![1] as any;
expect(payload.tools).toEqual([
expect.objectContaining({ name: "my_tool", skipPermission: true }),
]);
spy.mockRestore();
});
});

describe("agent parameter in session creation", () => {
it("forwards agent in session.create request", async () => {
const client = new CopilotClient();
Expand Down
10 changes: 10 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,16 @@ async def edit_file(params: EditFileParams) -> str:
# your logic
```

#### Skipping Permission Prompts

Set `skip_permission=True` on a tool definition to allow it to execute without triggering a permission prompt:

```python
@define_tool(name="safe_lookup", description="A read-only lookup that needs no confirmation", skip_permission=True)
async def safe_lookup(params: LookupParams) -> str:
# your logic
```

## Image Support

The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path:
Expand Down
4 changes: 4 additions & 0 deletions python/copilot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,8 @@ async def create_session(self, config: SessionConfig) -> CopilotSession:
definition["parameters"] = tool.parameters
if tool.overrides_built_in_tool:
definition["overridesBuiltInTool"] = True
if tool.skip_permission:
definition["skipPermission"] = True
tool_defs.append(definition)

payload: dict[str, Any] = {}
Expand Down Expand Up @@ -697,6 +699,8 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) ->
definition["parameters"] = tool.parameters
if tool.overrides_built_in_tool:
definition["overridesBuiltInTool"] = True
if tool.skip_permission:
definition["skipPermission"] = True
tool_defs.append(definition)

payload: dict[str, Any] = {"sessionId": session_id}
Expand Down
4 changes: 4 additions & 0 deletions python/copilot/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def define_tool(
*,
description: str | None = None,
overrides_built_in_tool: bool = False,
skip_permission: bool = False,
) -> Callable[[Callable[..., Any]], Tool]: ...


Expand All @@ -37,6 +38,7 @@ def define_tool(
handler: Callable[[T, ToolInvocation], R],
params_type: type[T],
overrides_built_in_tool: bool = False,
skip_permission: bool = False,
) -> Tool: ...


Expand All @@ -47,6 +49,7 @@ def define_tool(
handler: Callable[[Any, ToolInvocation], Any] | None = None,
params_type: type[BaseModel] | None = None,
overrides_built_in_tool: bool = False,
skip_permission: bool = False,
) -> Tool | Callable[[Callable[[Any, ToolInvocation], Any]], Tool]:
"""
Define a tool with automatic JSON schema generation from Pydantic models.
Expand Down Expand Up @@ -154,6 +157,7 @@ async def wrapped_handler(invocation: ToolInvocation) -> ToolResult:
parameters=schema,
handler=wrapped_handler,
overrides_built_in_tool=overrides_built_in_tool,
skip_permission=skip_permission,
)

# If handler is provided, call decorator immediately
Expand Down
1 change: 1 addition & 0 deletions python/copilot/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ class Tool:
handler: ToolHandler
parameters: dict[str, Any] | None = None
overrides_built_in_tool: bool = False
skip_permission: bool = False


# System message configuration (discriminated union)
Expand Down
Loading