Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 307982a

Browse files
authored
feat: Add tool restriction flags for non-interactive mode (anomalyco#29)
1 parent ba416e7 commit 307982a

7 files changed

Lines changed: 372 additions & 105 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,5 @@ Thumbs.db
4242
.env.local
4343

4444
.opencode/
45-
45+
# ignore locally built binary
46+
opencode*

README.md

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,28 @@ opencode -p "Explain the use of context in Go" -f json
225225

226226
# Run without showing the spinner
227227
opencode -p "Explain the use of context in Go" -q
228+
229+
# Enable verbose logging to stderr
230+
opencode -p "Explain the use of context in Go" --verbose
231+
232+
# Restrict the agent to only use specific tools
233+
opencode -p "Explain the use of context in Go" --allowedTools=view,ls,glob
234+
235+
# Prevent the agent from using specific tools
236+
opencode -p "Explain the use of context in Go" --excludedTools=bash,edit
228237
```
229238

230239
In this mode, OpenCode will process your prompt, print the result to standard output, and then exit. All permissions are auto-approved for the session.
231240

241+
### Tool Restrictions
242+
243+
You can control which tools the AI assistant has access to in non-interactive mode:
244+
245+
- `--allowedTools`: Comma-separated list of tools that the agent is allowed to use. Only these tools will be available.
246+
- `--excludedTools`: Comma-separated list of tools that the agent is not allowed to use. All other tools will be available.
247+
248+
These flags are mutually exclusive - you can use either `--allowedTools` or `--excludedTools`, but not both at the same time.
249+
232250
### Output Formats
233251

234252
OpenCode supports the following output formats in non-interactive mode:
@@ -242,14 +260,17 @@ The output format is implemented as a strongly-typed `OutputFormat` in the codeb
242260

243261
## Command-line Flags
244262

245-
| Flag | Short | Description |
246-
| ----------------- | ----- | ------------------------------------------------------ |
247-
| `--help` | `-h` | Display help information |
248-
| `--debug` | `-d` | Enable debug mode |
249-
| `--cwd` | `-c` | Set current working directory |
250-
| `--prompt` | `-p` | Run a single prompt in non-interactive mode |
251-
| `--output-format` | `-f` | Output format for non-interactive mode (text, json) |
252-
| `--quiet` | `-q` | Hide spinner in non-interactive mode |
263+
| Flag | Short | Description |
264+
| ----------------- | ----- | ---------------------------------------------------------- |
265+
| `--help` | `-h` | Display help information |
266+
| `--debug` | `-d` | Enable debug mode |
267+
| `--cwd` | `-c` | Set current working directory |
268+
| `--prompt` | `-p` | Run a single prompt in non-interactive mode |
269+
| `--output-format` | `-f` | Output format for non-interactive mode (text, json) |
270+
| `--quiet` | `-q` | Hide spinner in non-interactive mode |
271+
| `--verbose` | | Display logs to stderr in non-interactive mode |
272+
| `--allowedTools` | | Restrict the agent to only use specified tools |
273+
| `--excludedTools` | | Prevent the agent from using specified tools |
253274

254275
## Keyboard Shortcuts
255276

cmd/non_interactive_mode.go

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os"
8+
"sync"
9+
"time"
10+
11+
"log/slog"
12+
13+
charmlog "github.com/charmbracelet/log"
14+
"github.com/sst/opencode/internal/app"
15+
"github.com/sst/opencode/internal/config"
16+
"github.com/sst/opencode/internal/db"
17+
"github.com/sst/opencode/internal/format"
18+
"github.com/sst/opencode/internal/llm/agent"
19+
"github.com/sst/opencode/internal/llm/tools"
20+
"github.com/sst/opencode/internal/message"
21+
"github.com/sst/opencode/internal/permission"
22+
"github.com/sst/opencode/internal/tui/components/spinner"
23+
"github.com/sst/opencode/internal/tui/theme"
24+
)
25+
26+
// syncWriter is a thread-safe writer that prevents interleaved output
27+
type syncWriter struct {
28+
w io.Writer
29+
mu sync.Mutex
30+
}
31+
32+
// Write implements io.Writer
33+
func (sw *syncWriter) Write(p []byte) (n int, err error) {
34+
sw.mu.Lock()
35+
defer sw.mu.Unlock()
36+
return sw.w.Write(p)
37+
}
38+
39+
// newSyncWriter creates a new synchronized writer
40+
func newSyncWriter(w io.Writer) io.Writer {
41+
return &syncWriter{w: w}
42+
}
43+
44+
// filterTools filters the provided tools based on allowed or excluded tool names
45+
func filterTools(allTools []tools.BaseTool, allowedTools, excludedTools []string) []tools.BaseTool {
46+
// If neither allowed nor excluded tools are specified, return all tools
47+
if len(allowedTools) == 0 && len(excludedTools) == 0 {
48+
return allTools
49+
}
50+
51+
// Create a map for faster lookups
52+
allowedMap := make(map[string]bool)
53+
for _, name := range allowedTools {
54+
allowedMap[name] = true
55+
}
56+
57+
excludedMap := make(map[string]bool)
58+
for _, name := range excludedTools {
59+
excludedMap[name] = true
60+
}
61+
62+
var filteredTools []tools.BaseTool
63+
64+
for _, tool := range allTools {
65+
toolName := tool.Info().Name
66+
67+
// If we have an allowed list, only include tools in that list
68+
if len(allowedTools) > 0 {
69+
if allowedMap[toolName] {
70+
filteredTools = append(filteredTools, tool)
71+
}
72+
} else if len(excludedTools) > 0 {
73+
// If we have an excluded list, include all tools except those in the list
74+
if !excludedMap[toolName] {
75+
filteredTools = append(filteredTools, tool)
76+
}
77+
}
78+
}
79+
80+
return filteredTools
81+
}
82+
83+
// handleNonInteractiveMode processes a single prompt in non-interactive mode
84+
func handleNonInteractiveMode(ctx context.Context, prompt string, outputFormat format.OutputFormat, quiet bool, verbose bool, allowedTools, excludedTools []string) error {
85+
// Initial log message using standard slog
86+
slog.Info("Running in non-interactive mode", "prompt", prompt, "format", outputFormat, "quiet", quiet, "verbose", verbose,
87+
"allowedTools", allowedTools, "excludedTools", excludedTools)
88+
89+
// Sanity check for mutually exclusive flags
90+
if quiet && verbose {
91+
return fmt.Errorf("--quiet and --verbose flags cannot be used together")
92+
}
93+
94+
// Set up logging to stderr if verbose mode is enabled
95+
if verbose {
96+
// Create a synchronized writer to prevent interleaved output
97+
syncWriter := newSyncWriter(os.Stderr)
98+
99+
// Create a charmbracelet/log logger that writes to the synchronized writer
100+
charmLogger := charmlog.NewWithOptions(syncWriter, charmlog.Options{
101+
Level: charmlog.DebugLevel,
102+
ReportCaller: true,
103+
ReportTimestamp: true,
104+
TimeFormat: time.RFC3339,
105+
Prefix: "OpenCode",
106+
})
107+
108+
// Set the global logger for charmbracelet/log
109+
charmlog.SetDefault(charmLogger)
110+
111+
// Create a slog handler that uses charmbracelet/log
112+
// This will forward all slog logs to charmbracelet/log
113+
slog.SetDefault(slog.New(charmLogger))
114+
115+
// Log a message to confirm verbose logging is enabled
116+
charmLogger.Info("Verbose logging enabled")
117+
}
118+
119+
// Start spinner if not in quiet mode
120+
var s *spinner.Spinner
121+
if !quiet {
122+
// Get the current theme to style the spinner
123+
currentTheme := theme.CurrentTheme()
124+
125+
// Create a themed spinner
126+
if currentTheme != nil {
127+
// Use the primary color from the theme
128+
s = spinner.NewThemedSpinner("Thinking...", currentTheme.Primary())
129+
} else {
130+
// Fallback to default spinner if no theme is available
131+
s = spinner.NewSpinner("Thinking...")
132+
}
133+
134+
s.Start()
135+
defer s.Stop()
136+
}
137+
138+
// Connect DB, this will also run migrations
139+
conn, err := db.Connect()
140+
if err != nil {
141+
return err
142+
}
143+
144+
// Create a context with cancellation
145+
ctx, cancel := context.WithCancel(ctx)
146+
defer cancel()
147+
148+
// Create the app
149+
app, err := app.New(ctx, conn)
150+
if err != nil {
151+
slog.Error("Failed to create app", "error", err)
152+
return err
153+
}
154+
155+
// Create a new session for this prompt
156+
session, err := app.Sessions.Create(ctx, "Non-interactive prompt")
157+
if err != nil {
158+
return fmt.Errorf("failed to create session: %w", err)
159+
}
160+
161+
// Set the session as current
162+
app.CurrentSession = &session
163+
164+
// Auto-approve all permissions for this session
165+
permission.AutoApproveSession(ctx, session.ID)
166+
167+
// Create the user message
168+
_, err = app.Messages.Create(ctx, session.ID, message.CreateMessageParams{
169+
Role: message.User,
170+
Parts: []message.ContentPart{message.TextContent{Text: prompt}},
171+
})
172+
if err != nil {
173+
return fmt.Errorf("failed to create message: %w", err)
174+
}
175+
176+
// If tool restrictions are specified, create a new agent with filtered tools
177+
if len(allowedTools) > 0 || len(excludedTools) > 0 {
178+
// Initialize MCP tools synchronously to ensure they're included in filtering
179+
mcpCtx, mcpCancel := context.WithTimeout(ctx, 10*time.Second)
180+
agent.GetMcpTools(mcpCtx, app.Permissions)
181+
mcpCancel()
182+
183+
// Get all available tools including MCP tools
184+
allTools := agent.PrimaryAgentTools(
185+
app.Permissions,
186+
app.Sessions,
187+
app.Messages,
188+
app.History,
189+
app.LSPClients,
190+
)
191+
192+
// Filter tools based on allowed/excluded lists
193+
filteredTools := filterTools(allTools, allowedTools, excludedTools)
194+
195+
// Log the filtered tools for debugging
196+
var toolNames []string
197+
for _, tool := range filteredTools {
198+
toolNames = append(toolNames, tool.Info().Name)
199+
}
200+
slog.Debug("Using filtered tools", "count", len(filteredTools), "tools", toolNames)
201+
202+
// Create a new agent with the filtered tools
203+
restrictedAgent, err := agent.NewAgent(
204+
config.AgentPrimary,
205+
app.Sessions,
206+
app.Messages,
207+
filteredTools,
208+
)
209+
if err != nil {
210+
return fmt.Errorf("failed to create restricted agent: %w", err)
211+
}
212+
213+
// Use the restricted agent for this request
214+
eventCh, err := restrictedAgent.Run(ctx, session.ID, prompt)
215+
if err != nil {
216+
return fmt.Errorf("failed to run restricted agent: %w", err)
217+
}
218+
219+
// Wait for the response
220+
var response message.Message
221+
for event := range eventCh {
222+
if event.Err() != nil {
223+
return fmt.Errorf("agent error: %w", event.Err())
224+
}
225+
response = event.Response()
226+
}
227+
228+
// Format and print the output
229+
content := ""
230+
if textContent := response.Content(); textContent != nil {
231+
content = textContent.Text
232+
}
233+
234+
formattedOutput, err := format.FormatOutput(content, outputFormat)
235+
if err != nil {
236+
return fmt.Errorf("failed to format output: %w", err)
237+
}
238+
239+
// Stop spinner before printing output
240+
if !quiet && s != nil {
241+
s.Stop()
242+
}
243+
244+
// Print the formatted output to stdout
245+
fmt.Println(formattedOutput)
246+
247+
// Shutdown the app
248+
app.Shutdown()
249+
250+
return nil
251+
}
252+
253+
// Run the default agent if no tool restrictions
254+
eventCh, err := app.PrimaryAgent.Run(ctx, session.ID, prompt)
255+
if err != nil {
256+
return fmt.Errorf("failed to run agent: %w", err)
257+
}
258+
259+
// Wait for the response
260+
var response message.Message
261+
for event := range eventCh {
262+
if event.Err() != nil {
263+
return fmt.Errorf("agent error: %w", event.Err())
264+
}
265+
response = event.Response()
266+
}
267+
268+
// Get the text content from the response
269+
content := ""
270+
if textContent := response.Content(); textContent != nil {
271+
content = textContent.Text
272+
}
273+
274+
// Format the output according to the specified format
275+
formattedOutput, err := format.FormatOutput(content, outputFormat)
276+
if err != nil {
277+
return fmt.Errorf("failed to format output: %w", err)
278+
}
279+
280+
// Stop spinner before printing output
281+
if !quiet && s != nil {
282+
s.Stop()
283+
}
284+
285+
// Print the formatted output to stdout
286+
fmt.Println(formattedOutput)
287+
288+
// Shutdown the app
289+
app.Shutdown()
290+
291+
return nil
292+
}

0 commit comments

Comments
 (0)