Skip to content

Commit 1af80ec

Browse files
authored
feat(trusted-tools): auto-execute trusted tool calls (#1556)
Add `trusted_tools` config option to allow automatic execution of trusted tool calls without manual approval. By default, all tool calls require approval, but users can now trust specific tools, groups, or all tools. Update README and type annotations to document the new behavior. Trusted tools are determined by function definition, group, or name. Closes #1534
1 parent d1d767c commit 1af80ec

8 files changed

Lines changed: 359 additions & 157 deletions

File tree

README.md

Lines changed: 117 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6
1919
CopilotChat.nvim brings GitHub Copilot Chat capabilities directly into Neovim with a focus on transparency and user control.
2020

2121
- 🤖 **Multiple AI Models** - GitHub Copilot (including GPT-4o, Gemini 2.5 Pro, Claude 4 Sonnet, Claude 3.7 Sonnet, Claude 3.5 Sonnet, o3-mini, o4-mini) + custom providers (Ollama, Mistral.ai). The exact list of available models depends on your [GitHub Copilot settings](https://github.com/settings/copilot/features) and the models provided by GitHub's API.
22-
- 🔧 **Tool Calling** - LLM can call workspace functions (file reading, git operations, search) with your explicit approval
22+
- 🔧 **Tool Calling** - LLM can call workspace functions (file reading, git operations, search) with manual approval or automatic execution for trusted tools
2323
- 🔒 **Privacy First** - Only shares what you explicitly request - no background data collection
2424
- 📝 **Interactive Chat** - Interactive UI with completion, diffs, and quickfix integration
2525
- 🎯 **Smart Prompts** - Composable templates and sticky prompts for consistent context
@@ -92,7 +92,7 @@ EOF
9292
# Core Concepts
9393

9494
- **Resources** (`#<name>`) - Add specific content (files, git diffs, URLs) to your prompt
95-
- **Tools** (`@<name>`) - Give LLM access to functions it can call with your approval
95+
- **Tools** (`@<name>`) - Give LLM access to functions it can call during the chat, with manual approval by default
9696
- **Sticky Prompts** (`> <text>`) - Persist context across single chat session
9797
- **Models** (`$<model>`) - Specify which AI model to use for the chat
9898
- **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks
@@ -114,7 +114,15 @@ EOF
114114
> You are a helpful coding assistant
115115
```
116116

117-
When you use `@copilot`, the LLM can call functions like `bash`, `edit`, `file`, `glob`, `grep`, `gitdiff` etc. You'll see the proposed function call and can approve/reject it before execution.
117+
When you use `@copilot`, the LLM can call functions from the `copilot` group such as `bash`, `edit`, `file`, `glob`, `grep`, and `gitdiff`.
118+
119+
- By default, proposed tool calls wait for your approval.
120+
- You can configure `trusted_tools` to automatically run specific tools or groups.
121+
- Resources added with `#...` are resolved immediately and shared as context.
122+
- Tool call results are sent back to the model as plain output, while manual resources keep their `##<uri>` references in chat.
123+
124+
> [!WARNING]
125+
> `trusted_tools = true` allows the model to run every enabled tool without asking. Only use it if you fully trust the tool set and workspace.
118126
119127
# Usage
120128

@@ -136,21 +144,20 @@ When you use `@copilot`, the LLM can call functions like `bash`, `edit`, `file`,
136144

137145
## Chat Key Mappings
138146

139-
| Insert | Normal | Action |
140-
| ------- | ------- | ------------------------------------------ |
141-
| `<Tab>` | - | Trigger/accept completion menu for tokens |
142-
| `<C-c>` | `q` | Close the chat window |
143-
| `<C-l>` | `<C-l>` | Reset and clear the chat window |
144-
| `<C-s>` | `<CR>` | Submit the current prompt |
145-
| - | `grr` | Toggle sticky prompt for line under cursor |
146-
| `<C-y>` | `<C-y>` | Accept nearest diff |
147-
| - | `gj` | Jump to section of nearest diff |
148-
| - | `gqa` | Add all answers from chat to quickfix list |
149-
| - | `gqd` | Add all diffs from chat to quickfix list |
150-
| - | `gy` | Yank nearest diff to register |
151-
| - | `gd` | Show diff between source and nearest diff |
152-
| - | `gc` | Show info about current chat |
153-
| - | `gh` | Show help message |
147+
| Insert | Normal | Action |
148+
| ------- | ------- | ----------------------------------------- |
149+
| `<Tab>` | - | Trigger/accept completion menu for tokens |
150+
| `<C-c>` | `q` | Close the chat window |
151+
| `<C-l>` | `<C-l>` | Reset and clear the chat window |
152+
| `<C-s>` | `<CR>` | Submit the current prompt |
153+
| `<C-y>` | `<C-y>` | Accept nearest diff |
154+
| - | `gj` | Jump to section of nearest diff |
155+
| - | `gqa` | Add all answers from chat to quickfix |
156+
| - | `gqd` | Add all diffs from chat to quickfix |
157+
| - | `gy` | Yank nearest diff to register |
158+
| - | `gd` | Show diff between source and nearest diff |
159+
| - | `gc` | Show info about current chat |
160+
| - | `gh` | Show help message |
154161

155162
> [!WARNING]
156163
> Some plugins (e.g. `copilot.vim`) may also map common keys like `<Tab>` in insert mode.
@@ -167,23 +174,24 @@ When you use `@copilot`, the LLM can call functions like `bash`, `edit`, `file`,
167174
168175
All predefined functions belong to the `copilot` group.
169176
170-
| Function | Type | Description | Example Usage |
171-
| ----------- | -------- | ------------------------------------------------------ | -------------------- |
172-
| `bash` | Tool | Executes a bash command and returns output | `@copilot` only |
173-
| `buffer` | Resource | Retrieves content from buffer(s) with diagnostics | `#buffer:active` |
174-
| `clipboard` | Resource | Provides access to system clipboard content | `#clipboard` |
175-
| `edit` | Tool | Applies a unified diff to a file | `@copilot` only |
176-
| `file` | Resource | Reads content from a specified file path | `#file:path/to/file` |
177-
| `gitdiff` | Resource | Retrieves git diff information | `#gitdiff:staged` |
178-
| `glob` | Resource | Lists filenames matching a pattern in workspace | `#glob:**/*.lua` |
179-
| `grep` | Resource | Searches for a pattern across files in workspace | `#grep:TODO` |
180-
| `selection` | Resource | Includes the current visual selection with diagnostics | `#selection` |
181-
| `url` | Resource | Fetches content from a specified URL | `#url:https://...` |
177+
| Function | Manual `#...` | Description | Example Usage |
178+
| ----------- | ------------- | ------------------------------------------------------ | -------------------- |
179+
| `bash` | No | Executes a bash command and returns output | `@copilot` |
180+
| `buffer` | Yes | Retrieves content from buffer(s) with diagnostics | `#buffer:active` |
181+
| `clipboard` | Yes | Provides access to system clipboard content | `#clipboard` |
182+
| `edit` | No | Applies a unified diff to a file | `@copilot` |
183+
| `file` | Yes | Reads content from a specified file path | `#file:path/to/file` |
184+
| `gitdiff` | Yes | Retrieves git diff information | `#gitdiff:staged` |
185+
| `glob` | Yes | Lists filenames matching a pattern in workspace | `#glob:**/*.lua` |
186+
| `grep` | Yes | Searches for a pattern across files in workspace | `#grep:TODO` |
187+
| `selection` | Yes | Includes the current visual selection with diagnostics | `#selection` |
188+
| `url` | Yes | Fetches content from a specified URL | `#url:https://...` |
189+
190+
`#...` resolves a function immediately and adds its output as chat context.
182191
183-
**Type Legend:**
192+
`@copilot` shares the enabled functions with the model so it can choose when to call them.
184193
185-
- **Resource**: Can be used manually via `#function` syntax
186-
- **Tool**: Can only be called by LLM via `@copilot` (for safety/complexity reasons)
194+
Only `bash` and `edit` are tool-only. The rest can be used both as manual resources and as callable tools.
187195
188196
## Predefined Prompts
189197
@@ -209,6 +217,7 @@ Most users only need to configure a few options:
209217
{
210218
model = 'gpt-4.1', -- AI model to use
211219
temperature = 0.1, -- Lower = focused, higher = creative
220+
trusted_tools = nil, -- Require approval for all tool calls
212221
window = {
213222
layout = 'vertical', -- 'vertical', 'horizontal', 'float'
214223
width = 0.5, -- 50% of screen width
@@ -241,12 +250,14 @@ Most users only need to configure a few options:
241250
}
242251
```
243252

253+
`window.layout` also supports `'replace'` to reuse the current window.
254+
244255
## Buffer Behavior
245256

246257
```lua
247258
-- Auto-command to customize chat buffer behavior
248259
vim.api.nvim_create_autocmd('BufEnter', {
249-
pattern = 'copilot-*',
260+
pattern = 'copilot-chat',
250261
callback = function()
251262
vim.opt_local.relativenumber = false
252263
vim.opt_local.number = false
@@ -278,6 +289,7 @@ Types of copilot highlights:
278289
- `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-4.1`)
279290
- `CopilotChatUri` - URI highlight in chat buffer (e.g. `##https://...`)
280291
- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body)
292+
- `CopilotChatAnnotationHeader` - Annotation header highlight in chat buffer
281293

282294
## Prompts
283295

@@ -304,14 +316,44 @@ Define your own prompts in the configuration:
304316

305317
## Functions
306318

319+
Use `trusted_tools` to control which tool calls are executed automatically:
320+
321+
```lua
322+
{
323+
trusted_tools = nil, -- default: require approval for all tool calls
324+
325+
-- trust all functions in a group
326+
-- trusted_tools = 'copilot',
327+
328+
-- trust specific functions by name or groups by name
329+
-- trusted_tools = { 'file', 'glob', 'grep' },
330+
331+
-- trust every enabled tool call
332+
-- trusted_tools = true,
333+
}
334+
```
335+
336+
A tool is trusted when any of these match:
337+
338+
- Its function definition sets `trusted = true`
339+
- Its function name appears in `trusted_tools`
340+
- Its function group appears in `trusted_tools`
341+
- `trusted_tools = true`
342+
343+
For most setups, trusting a few read-only functions such as `file`, `glob`, or `grep` is safer than trusting everything.
344+
345+
> [!WARNING]
346+
> Trusted tools run without asking for confirmation. Be especially careful with tools like `bash` and `edit`, which can change your workspace.
347+
307348
Define your own functions in the configuration with input handling and schema:
308349

309350
```lua
310351
{
311352
functions = {
312353
birthday = {
313-
description = "Retrieves birthday information for a person",
314-
uri = "birthday://{name}",
354+
description = 'Retrieves birthday information for a person',
355+
uri = 'birthday://{name}',
356+
trusted = false,
315357
schema = {
316358
type = 'object',
317359
required = { 'name' },
@@ -329,14 +371,16 @@ Define your own functions in the configuration with input handling and schema:
329371
uri = 'birthday://' .. input.name,
330372
mimetype = 'text/plain',
331373
data = input.name .. ' birthday info',
332-
}
374+
},
333375
}
334-
end
335-
}
376+
end,
377+
},
336378
}
337379
}
338380
```
339381

382+
If a function has a `uri`, it can be used manually with `#birthday:Alice`. Functions without a `uri` are tool-only and can only be called by the model.
383+
340384
## Providers
341385

342386
Add custom AI providers:
@@ -345,9 +389,9 @@ Add custom AI providers:
345389
{
346390
providers = {
347391
my_provider = {
348-
get_url = function(opts) return "https://api.example.com/chat" end,
349-
get_headers = function() return { ["Authorization"] = "Bearer " .. api_key } end,
350-
get_models = function() return { { id = "gpt-4.1", name = "GPT-4.1 model" } } end,
392+
get_url = function(opts) return 'https://api.example.com/chat' end,
393+
get_headers = function() return { ['Authorization'] = 'Bearer ' .. api_key } end,
394+
get_models = function() return { { id = 'gpt-4.1', name = 'GPT-4.1 model' } } end,
351395
prepare_input = require('CopilotChat.config.providers').copilot.prepare_input,
352396
prepare_output = require('CopilotChat.config.providers').copilot.prepare_output,
353397
}
@@ -363,7 +407,7 @@ Add custom AI providers:
363407
disabled?: boolean,
364408

365409
-- Optional: Extra info about the provider displayed in info panel
366-
get_info?(): string[]
410+
get_info?(headers: table): string[]
367411

368412
-- Optional: Get extra request headers with optional expiration time
369413
get_headers?(): table<string,string>, number?,
@@ -379,20 +423,23 @@ Add custom AI providers:
379423

380424
-- Optional: Get available models
381425
get_models?(headers: table): table<CopilotChat.Provider.model>,
426+
427+
-- Optional: Resolve a user-facing model id to a provider model id
428+
resolve_model?(headers: table, model: string): string,
382429
}
383430
```
384431

385432
**Built-in providers:**
386433

387434
- `copilot` - GitHub Copilot (default)
388-
- `github_models` - GitHub Marketplace models (disabled by default)
435+
- `github_models` - GitHub Models (disabled by default)
389436

390437
# API Reference
391438

392439
## Core
393440

394441
```lua
395-
local chat = require("CopilotChat")
442+
local chat = require('CopilotChat')
396443

397444
-- Basic Chat Functions
398445
chat.ask(prompt, config) -- Ask a question with optional config
@@ -422,7 +469,7 @@ chat.log_level(level) -- Set log level (debug, info, etc.)
422469
You can also access the chat window UI methods through the `chat.chat` object:
423470

424471
```lua
425-
local window = require("CopilotChat").chat
472+
local window = require('CopilotChat').chat
426473

427474
-- Chat UI State
428475
window:visible() -- Check if chat window is visible
@@ -441,8 +488,8 @@ window:start() -- Start writing to chat window
441488
window:finish() -- Finish writing to chat window
442489

443490
-- Source Management
444-
window.get_source() -- Get the current source buffer and window
445-
window.set_source(winnr) -- Set the source window
491+
window:get_source() -- Get the current source buffer and window
492+
window:set_source(winnr) -- Set the source window
446493

447494
-- Navigation
448495
window:follow() -- Move cursor to end of chat content
@@ -455,33 +502,38 @@ window:overlay(opts) -- Show overlay with specified options
455502
## Prompt parser
456503

457504
```lua
458-
local parser = require("CopilotChat.prompts")
505+
local parser = require('CopilotChat.prompts')
459506

460507
parser.resolve_prompt() -- Resolve prompt references
461-
parser.resolve_tools() -- Resolve tools that are available for automatic use by LLM
508+
parser.resolve_tools() -- Resolve tools shared with the model via @...
509+
parser.resolve_functions() -- Resolve manual function/resource references via #...
462510
parser.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run)
463511
```
464512

465513
## Example Usage
466514

467515
```lua
468516
-- Open chat, ask a question and handle response
469-
require("CopilotChat").open()
470-
require("CopilotChat").ask("#buffer Explain this code", {
517+
require('CopilotChat').open()
518+
require('CopilotChat').ask('#buffer Explain this code', {
471519
callback = function(response)
472-
vim.notify("Got response: " .. response:sub(1, 50) .. "...")
473-
return response
520+
vim.notify('Got response: ' .. vim.trim(response.content):sub(1, 50) .. '...')
474521
end,
475522
})
476523

477524
-- Save and load chat history
478-
require("CopilotChat").save("my_debugging_session")
479-
require("CopilotChat").load("my_debugging_session")
525+
require('CopilotChat').save('my_debugging_session')
526+
require('CopilotChat').load('my_debugging_session')
480527

481528
-- Use custom sticky and model
482-
require("CopilotChat").ask("How can I optimize this?", {
483-
model = "gpt-4.1",
484-
sticky = {"#buffer", "#gitdiff:staged"}
529+
require('CopilotChat').ask('How can I optimize this?', {
530+
model = 'gpt-4.1',
531+
sticky = { '#buffer', '#gitdiff:staged' },
532+
})
533+
534+
-- Automatically trust a small read-only tool set
535+
require('CopilotChat').setup({
536+
trusted_tools = { 'file', 'glob', 'grep' },
485537
})
486538
```
487539

@@ -512,6 +564,12 @@ To run tests:
512564
make test
513565
```
514566

567+
To run the same formatting check as CI:
568+
569+
```bash
570+
stylua --check .
571+
```
572+
515573
## Contributing
516574

517575
1. Fork the repository

lua/CopilotChat/config.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
---@field tools string|table<string>|nil
2020
---@field resources string|table<string>|nil
2121
---@field sticky string|table<string>|nil
22+
---@field trusted_tools boolean|string|table<string>|nil
2223
---@field diff 'block'|'unified'?
2324
---@field language string?
2425
---@field temperature number?
@@ -64,6 +65,7 @@ return {
6465
tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @).
6566
resources = 'selection', -- Default resources to share with LLM (can be specified manually in prompt via #).
6667
sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >).
68+
trusted_tools = nil, -- Trust tool calls from specific functions or groups, or all trusted tools when true (e.g., {'buffer', 'file'} or 'copilot').
6769
diff = 'block', -- Default diff format to use, 'block' or 'unified'.
6870
language = 'English', -- Default language to use for answers
6971

lua/CopilotChat/config/functions.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ end
4444
---@field description string?
4545
---@field schema table?
4646
---@field group string?
47+
---@field trusted boolean?
4748
---@field uri string?
4849
---@field resolve fun(input: table, source: CopilotChat.ui.chat.Source):CopilotChat.client.Resource[]
4950

0 commit comments

Comments
 (0)